@cleocode/core 2026.6.14 → 2026.6.17
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/check/pr-gate.d.ts +6 -2
- package/dist/check/pr-gate.d.ts.map +1 -1
- package/dist/check/pr-gate.js +14 -32
- package/dist/check/pr-gate.js.map +1 -1
- package/dist/config/config-repair.d.ts +70 -0
- package/dist/config/config-repair.d.ts.map +1 -0
- package/dist/config/config-repair.js +248 -0
- package/dist/config/config-repair.js.map +1 -0
- package/dist/config/registry.d.ts.map +1 -1
- package/dist/config/registry.js +4 -4
- package/dist/config/registry.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -10
- package/dist/config.js.map +1 -1
- package/dist/docs/export-document.js +58 -16
- package/dist/docs/export-document.js.map +2 -2
- package/dist/gc/cleanup.d.ts +23 -8
- package/dist/gc/cleanup.d.ts.map +1 -1
- package/dist/gc/cleanup.js +287 -22
- package/dist/gc/cleanup.js.map +1 -1
- package/dist/gc/janitor.d.ts +149 -0
- package/dist/gc/janitor.d.ts.map +1 -0
- package/dist/gc/janitor.js +760 -0
- package/dist/gc/janitor.js.map +1 -0
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +3 -0
- package/dist/internal.js.map +1 -1
- package/dist/llm/cli-ops.d.ts +31 -4
- package/dist/llm/cli-ops.d.ts.map +1 -1
- package/dist/llm/cli-ops.js +110 -26
- package/dist/llm/cli-ops.js.map +1 -1
- package/dist/llm/credential-pool.d.ts +62 -0
- package/dist/llm/credential-pool.d.ts.map +1 -1
- package/dist/llm/credential-pool.js +161 -0
- package/dist/llm/credential-pool.js.map +1 -1
- package/dist/llm/cross-provider-selector.d.ts +254 -0
- package/dist/llm/cross-provider-selector.d.ts.map +1 -0
- package/dist/llm/cross-provider-selector.js +635 -0
- package/dist/llm/cross-provider-selector.js.map +1 -0
- package/dist/llm/index.d.ts +2 -0
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +1 -0
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/local-model-fit.d.ts +260 -0
- package/dist/llm/local-model-fit.d.ts.map +1 -0
- package/dist/llm/local-model-fit.js +563 -0
- package/dist/llm/local-model-fit.js.map +1 -0
- package/dist/llm/plugin-facade.js +883 -231
- package/dist/llm/plugin-facade.js.map +3 -3
- package/dist/llm/provider-registry/builtin/ollama.d.ts +18 -0
- package/dist/llm/provider-registry/builtin/ollama.d.ts.map +1 -1
- package/dist/llm/provider-registry/builtin/ollama.js +20 -2
- package/dist/llm/provider-registry/builtin/ollama.js.map +1 -1
- package/dist/llm/role-resolver.d.ts.map +1 -1
- package/dist/llm/role-resolver.js +45 -10
- package/dist/llm/role-resolver.js.map +1 -1
- package/dist/llm/transports/codex-responses.d.ts +111 -88
- package/dist/llm/transports/codex-responses.d.ts.map +1 -1
- package/dist/llm/transports/codex-responses.js +456 -258
- package/dist/llm/transports/codex-responses.js.map +1 -1
- package/dist/release/index.d.ts +2 -2
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +1 -1
- package/dist/release/index.js.map +1 -1
- package/dist/release/reconcile.d.ts +97 -0
- package/dist/release/reconcile.d.ts.map +1 -1
- package/dist/release/reconcile.js +412 -5
- package/dist/release/reconcile.js.map +1 -1
- package/dist/resources/backend.d.ts +155 -0
- package/dist/resources/backend.d.ts.map +1 -0
- package/dist/resources/backend.js +23 -0
- package/dist/resources/backend.js.map +1 -0
- package/dist/resources/linux-backend.d.ts +167 -0
- package/dist/resources/linux-backend.d.ts.map +1 -0
- package/dist/resources/linux-backend.js +259 -0
- package/dist/resources/linux-backend.js.map +1 -0
- package/dist/resources/monitor.d.ts +230 -0
- package/dist/resources/monitor.d.ts.map +1 -0
- package/dist/resources/monitor.js +255 -0
- package/dist/resources/monitor.js.map +1 -0
- package/dist/resources/spawn-wrapper.d.ts +265 -0
- package/dist/resources/spawn-wrapper.d.ts.map +1 -0
- package/dist/resources/spawn-wrapper.js +304 -0
- package/dist/resources/spawn-wrapper.js.map +1 -0
- package/dist/selfimprove/budget.d.ts +11 -2
- package/dist/selfimprove/budget.d.ts.map +1 -1
- package/dist/selfimprove/budget.js +11 -2
- package/dist/selfimprove/budget.js.map +1 -1
- package/dist/selfimprove/fix-gen-context.d.ts +172 -0
- package/dist/selfimprove/fix-gen-context.d.ts.map +1 -0
- package/dist/selfimprove/fix-gen-context.js +218 -0
- package/dist/selfimprove/fix-gen-context.js.map +1 -0
- package/dist/selfimprove/fix-gen.d.ts +285 -0
- package/dist/selfimprove/fix-gen.d.ts.map +1 -0
- package/dist/selfimprove/fix-gen.js +346 -0
- package/dist/selfimprove/fix-gen.js.map +1 -0
- package/dist/selfimprove/op-source-map.d.ts +92 -0
- package/dist/selfimprove/op-source-map.d.ts.map +1 -0
- package/dist/selfimprove/op-source-map.js +187 -0
- package/dist/selfimprove/op-source-map.js.map +1 -0
- package/dist/selfimprove/probe-helper.d.ts +61 -0
- package/dist/selfimprove/probe-helper.d.ts.map +1 -0
- package/dist/selfimprove/probe-helper.js +64 -0
- package/dist/selfimprove/probe-helper.js.map +1 -0
- package/dist/selfimprove/run-loop.d.ts +44 -4
- package/dist/selfimprove/run-loop.d.ts.map +1 -1
- package/dist/selfimprove/run-loop.js +73 -6
- package/dist/selfimprove/run-loop.js.map +1 -1
- package/dist/selfimprove/scenarios/seeded-code-regression/golden.json +18 -0
- package/dist/selfimprove/scenarios/seeded-code-regression/scenario.json +12 -0
- package/dist/sentient/cross-project-hygiene.d.ts +11 -2
- package/dist/sentient/cross-project-hygiene.d.ts.map +1 -1
- package/dist/sentient/cross-project-hygiene.js +73 -10
- package/dist/sentient/cross-project-hygiene.js.map +1 -1
- package/dist/sentient/daemon.d.ts.map +1 -1
- package/dist/sentient/daemon.js +7 -2
- package/dist/sentient/daemon.js.map +1 -1
- package/dist/sentient/worktree-dispatch.d.ts.map +1 -1
- package/dist/sentient/worktree-dispatch.js +8 -1
- package/dist/sentient/worktree-dispatch.js.map +1 -1
- package/dist/setup/index.d.ts +1 -0
- package/dist/setup/index.d.ts.map +1 -1
- package/dist/setup/index.js +1 -0
- package/dist/setup/index.js.map +1 -1
- package/dist/setup/sections/models-roles.d.ts +32 -1
- package/dist/setup/sections/models-roles.d.ts.map +1 -1
- package/dist/setup/sections/models-roles.js +118 -19
- package/dist/setup/sections/models-roles.js.map +1 -1
- package/dist/setup/whoami-summary.d.ts +34 -0
- package/dist/setup/whoami-summary.d.ts.map +1 -0
- package/dist/setup/whoami-summary.js +106 -0
- package/dist/setup/whoami-summary.js.map +1 -0
- package/dist/spawn/branch-lock.d.ts.map +1 -1
- package/dist/spawn/branch-lock.js +7 -1
- package/dist/spawn/branch-lock.js.map +1 -1
- package/dist/store/attachment-repair.d.ts +110 -0
- package/dist/store/attachment-repair.d.ts.map +1 -0
- package/dist/store/attachment-repair.js +326 -0
- package/dist/store/attachment-repair.js.map +1 -0
- package/dist/store/attachment-store.d.ts.map +1 -1
- package/dist/store/attachment-store.js +24 -7
- package/dist/store/attachment-store.js.map +1 -1
- package/dist/tasks/evidence.d.ts +34 -2
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +293 -35
- package/dist/tasks/evidence.js.map +1 -1
- package/dist/verification/verify-tools.d.ts.map +1 -1
- package/dist/verification/verify-tools.js +5 -6
- package/dist/verification/verify-tools.js.map +1 -1
- package/package.json +22 -12
- package/templates/workflows/release-prepare.yml.tmpl +48 -2
|
@@ -1,55 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CodexResponsesTransport — OpenAI Responses API transport.
|
|
2
|
+
* CodexResponsesTransport — OpenAI Responses API transport (raw-fetch variant).
|
|
3
3
|
*
|
|
4
|
-
* Implements {@link LlmTransport} for
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - xAI (grok-* models via Responses-compatible endpoints)
|
|
4
|
+
* Implements {@link LlmTransport} for the ChatGPT Codex backend
|
|
5
|
+
* (`https://chatgpt.com/backend-api/codex/responses`) and any provider that
|
|
6
|
+
* speaks a compatible Responses-API SSE protocol (xAI grok-via-responses).
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
* - Input uses the `input` array of ResponseInputItem (not `messages`).
|
|
11
|
-
* - Tools use `{type: 'function', name, description, parameters}` shape.
|
|
12
|
-
* - Tool results use `{type: 'function_call_output', call_id, output}` items.
|
|
13
|
-
* - The system prompt goes in `instructions`, not a system message.
|
|
14
|
-
* - Response text is extracted from `output_text` shortcut or by scanning
|
|
15
|
-
* `output` for `{type: 'message'}` items.
|
|
16
|
-
* - Tool calls come from `output` items of `{type: 'function_call'}`.
|
|
17
|
-
* - Usage is in `response.usage.input_tokens` / `output_tokens`.
|
|
8
|
+
* ## Wire shape (mirrored from @earendil-works/pi-ai 0.78.x — imports banned)
|
|
18
9
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
10
|
+
* Unlike the previous OpenAI-SDK-based implementation, this transport uses
|
|
11
|
+
* **raw `fetch`** to control every request header. The Codex backend rejects
|
|
12
|
+
* requests that carry the SDK's default `Accept: application/json` header or
|
|
13
|
+
* that omit `store: false` in the body.
|
|
14
|
+
*
|
|
15
|
+
* Mandatory request headers:
|
|
16
|
+
* - `Authorization: Bearer <token>`
|
|
17
|
+
* - `chatgpt-account-id: <jwt claim>`
|
|
18
|
+
* - `originator: codex_cli_rs` (set by buildCodexOAuthHeaders)
|
|
19
|
+
* - `OpenAI-Beta: responses=experimental`
|
|
20
|
+
* - `accept: text/event-stream` (always — backend mandates stream:true on every request)
|
|
21
|
+
* - `content-type: application/json`
|
|
22
|
+
*
|
|
23
|
+
* Mandatory body fields:
|
|
24
|
+
* - `model`, `input`, `instructions`
|
|
25
|
+
* - `store: false` (codex backend rejects store:true or absent)
|
|
26
|
+
* - `stream: true` (always — the Codex backend rejects stream:false)
|
|
21
27
|
*
|
|
22
28
|
* @module llm/transports/codex-responses
|
|
29
|
+
* @task T11985
|
|
23
30
|
* @task T9311
|
|
24
31
|
* @epic T9261 (T-LLM-CRED-CENTRALIZATION Phase 5)
|
|
25
32
|
* @see ADR-072 §LlmTransport — pure wire level
|
|
26
33
|
*/
|
|
27
|
-
import { OpenAI as OpenAIClient } from 'openai';
|
|
28
34
|
// ---------------------------------------------------------------------------
|
|
29
35
|
// CodexResponsesTransport
|
|
30
36
|
// ---------------------------------------------------------------------------
|
|
31
37
|
/**
|
|
32
|
-
* Transport for providers that expose the OpenAI Responses API.
|
|
38
|
+
* Transport for providers that expose the OpenAI Responses API via raw fetch.
|
|
33
39
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
40
|
+
* Uses native `fetch` (not the OpenAI SDK) so that the `Accept` header,
|
|
41
|
+
* `store: false` body field, and `OpenAI-Beta` header can all be set
|
|
42
|
+
* explicitly — the OpenAI SDK hardcodes `Accept: application/json` and omits
|
|
43
|
+
* `store`, both of which cause the Codex ChatGPT backend to return a 400.
|
|
36
44
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* items when a prior `assistant` turn carries tool calls.
|
|
45
|
+
* SSE stream parsing mirrors the pi-ai 0.78.x reference implementation
|
|
46
|
+
* (`@earendil-works/pi-ai/dist/providers/openai-codex-responses.js`) —
|
|
47
|
+
* imports from that package are banned; the wire shape is transcribed here.
|
|
41
48
|
*
|
|
42
49
|
* @example
|
|
43
50
|
* ```ts
|
|
44
51
|
* const transport = new CodexResponsesTransport({
|
|
45
52
|
* provider: 'openai',
|
|
46
|
-
* apiKey: process.env.
|
|
47
|
-
*
|
|
48
|
-
* const response = await transport.complete({
|
|
49
|
-
* model: 'codex-mini-latest',
|
|
50
|
-
* messages: [{ role: 'user', content: 'Write a fizzbuzz.' }],
|
|
51
|
-
* maxTokens: 1024,
|
|
53
|
+
* apiKey: process.env.CODEX_OAUTH_TOKEN!,
|
|
54
|
+
* defaultHeaders: buildCodexOAuthHeaders(process.env.CODEX_OAUTH_TOKEN!),
|
|
52
55
|
* });
|
|
56
|
+
* for await (const delta of transport.stream({
|
|
57
|
+
* model: 'gpt-5.5',
|
|
58
|
+
* messages: [{ role: 'user', content: 'Reply with exactly: pong' }],
|
|
59
|
+
* maxTokens: 64,
|
|
60
|
+
* }, {})) {
|
|
61
|
+
* process.stdout.write(delta.text);
|
|
62
|
+
* }
|
|
53
63
|
* ```
|
|
54
64
|
*/
|
|
55
65
|
export class CodexResponsesTransport {
|
|
@@ -64,117 +74,211 @@ export class CodexResponsesTransport {
|
|
|
64
74
|
* @see ADR-072 §Type lock-in
|
|
65
75
|
*/
|
|
66
76
|
apiMode = 'codex_responses';
|
|
67
|
-
/**
|
|
68
|
-
|
|
77
|
+
/** API key / OAuth bearer token. */
|
|
78
|
+
_apiKey;
|
|
79
|
+
/**
|
|
80
|
+
* Resolved endpoint URL (the full `/codex/responses` path).
|
|
81
|
+
* Derived once at construction so the URL normalisation runs once.
|
|
82
|
+
*/
|
|
83
|
+
_endpointUrl;
|
|
84
|
+
/** Extra headers merged into every request (auth, originator, etc.). */
|
|
85
|
+
_defaultHeaders;
|
|
69
86
|
/**
|
|
70
87
|
* Construct a CodexResponsesTransport.
|
|
71
88
|
*
|
|
72
|
-
* @param opts - Construction options
|
|
73
|
-
*
|
|
89
|
+
* @param opts - Construction options: provider, API key, optional base URL
|
|
90
|
+
* and default headers (auth + Cloudflare-bypass headers for OAuth callers).
|
|
74
91
|
*/
|
|
75
92
|
constructor(opts) {
|
|
76
93
|
this.provider = opts.provider;
|
|
77
|
-
this.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
defaultHeaders: opts.defaultHeaders,
|
|
81
|
-
});
|
|
94
|
+
this._apiKey = opts.apiKey;
|
|
95
|
+
this._endpointUrl = resolveCodexUrl(opts.baseUrl);
|
|
96
|
+
this._defaultHeaders = opts.defaultHeaders ?? {};
|
|
82
97
|
}
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// LlmTransport — complete()
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
83
101
|
/**
|
|
84
|
-
* Execute a single completion using the
|
|
102
|
+
* Execute a single (non-streaming) completion using the Responses API.
|
|
85
103
|
*
|
|
86
|
-
*
|
|
87
|
-
* `
|
|
88
|
-
*
|
|
104
|
+
* The Codex ChatGPT backend mandates `stream: true` on every request and
|
|
105
|
+
* returns `400 {"detail":"Stream must be set to true"}` for `stream: false`.
|
|
106
|
+
* This method therefore always sends `stream: true` with
|
|
107
|
+
* `Accept: text/event-stream`, internally consumes the SSE stream, and
|
|
108
|
+
* aggregates text deltas + tool calls + usage into a {@link NormalizedResponse}
|
|
109
|
+
* — identical to calling `stream()` and collecting all chunks.
|
|
89
110
|
*
|
|
90
111
|
* @param request - Provider-neutral request parameters.
|
|
91
112
|
* @param _ctx - Transport context (unused; `request.signal` handles abort).
|
|
92
113
|
* @returns Normalized response envelope.
|
|
93
114
|
*/
|
|
94
115
|
async complete(request, _ctx) {
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
116
|
+
const body = this._buildBody(request, true);
|
|
117
|
+
const headers = this._buildHeaders(true);
|
|
118
|
+
const res = await fetch(this._endpointUrl, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers,
|
|
121
|
+
body: JSON.stringify(body),
|
|
122
|
+
signal: request.signal,
|
|
123
|
+
});
|
|
124
|
+
if (!res.ok) {
|
|
125
|
+
throw new Error(await buildHttpError(res));
|
|
126
|
+
}
|
|
127
|
+
if (!res.body) {
|
|
128
|
+
throw new Error('codex_responses: response body is null (no SSE stream)');
|
|
129
|
+
}
|
|
130
|
+
// Aggregate SSE stream into a single NormalizedResponse.
|
|
131
|
+
const textParts = [];
|
|
132
|
+
const toolCalls = [];
|
|
133
|
+
let finalUsage = null;
|
|
134
|
+
let finishReason = null;
|
|
135
|
+
let responseId = null;
|
|
136
|
+
let responseModel = null;
|
|
137
|
+
for await (const event of parseSSE(res.body, request.signal)) {
|
|
138
|
+
const evType = event['type'];
|
|
139
|
+
if (evType === 'response.output_text.delta') {
|
|
140
|
+
const delta = event['delta'];
|
|
141
|
+
if (delta)
|
|
142
|
+
textParts.push(delta);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (evType === 'response.output_item.done') {
|
|
146
|
+
// Capture function_call items emitted per-output-item.
|
|
147
|
+
const item = event['item'];
|
|
148
|
+
if (item?.['type'] === 'function_call') {
|
|
149
|
+
toolCalls.push({
|
|
150
|
+
id: typeof item['call_id'] === 'string' ? item['call_id'] : null,
|
|
151
|
+
name: typeof item['name'] === 'string' ? item['name'] : 'unknown',
|
|
152
|
+
arguments: typeof item['arguments'] === 'string' ? item['arguments'] : '{}',
|
|
153
|
+
providerData: {
|
|
154
|
+
call_id: item['call_id'],
|
|
155
|
+
response_item_id: item['id'],
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (evType === 'response.completed') {
|
|
162
|
+
const completedResponse = event['response'];
|
|
163
|
+
if (completedResponse) {
|
|
164
|
+
finishReason = completedResponse['status'] ?? 'completed';
|
|
165
|
+
responseId = completedResponse['id'] ?? null;
|
|
166
|
+
responseModel = completedResponse['model'] ?? null;
|
|
167
|
+
const usage = completedResponse['usage'];
|
|
168
|
+
if (usage)
|
|
169
|
+
finalUsage = normalizeUsage(usage);
|
|
170
|
+
// Also capture tool calls embedded in the completed output array.
|
|
171
|
+
const output = completedResponse['output'] ?? [];
|
|
172
|
+
const embeddedCalls = extractToolCallsFromOutput(output);
|
|
173
|
+
if (embeddedCalls.length > 0 && toolCalls.length === 0) {
|
|
174
|
+
toolCalls.push(...embeddedCalls);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
if (evType === 'response.failed' || evType === 'response.incomplete') {
|
|
180
|
+
const failedResponse = event['response'];
|
|
181
|
+
if (failedResponse) {
|
|
182
|
+
finishReason = failedResponse['status'] ?? evType;
|
|
183
|
+
const usage = failedResponse['usage'];
|
|
184
|
+
if (usage)
|
|
185
|
+
finalUsage = normalizeUsage(usage);
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
if (evType === 'error') {
|
|
190
|
+
const code = event['code'];
|
|
191
|
+
const message = event['message'];
|
|
192
|
+
throw new Error(`Codex error: ${message ?? code ?? JSON.stringify(event)}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const content = textParts.join('') || null;
|
|
196
|
+
return {
|
|
197
|
+
id: responseId ?? `resp-${Date.now().toString(36)}`,
|
|
198
|
+
model: responseModel ?? request.model,
|
|
199
|
+
content,
|
|
200
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : null,
|
|
201
|
+
stopReason: finishReason ?? 'completed',
|
|
202
|
+
usage: finalUsage ?? { inputTokens: 0, outputTokens: 0 },
|
|
203
|
+
providerData: {
|
|
204
|
+
codex_response_id: responseId,
|
|
205
|
+
},
|
|
206
|
+
raw: {},
|
|
102
207
|
};
|
|
103
|
-
if (request.system)
|
|
104
|
-
params['instructions'] = request.system;
|
|
105
|
-
if (tools.length > 0)
|
|
106
|
-
params['tools'] = tools;
|
|
107
|
-
// PARITY GAP: cacheStrategy (system_and_3 / prefix_and_2) not yet supported
|
|
108
|
-
// by OpenAI Responses API — it has its own `prompt_cache_retention` parameter
|
|
109
|
-
// but no equivalent to Anthropic's cache-breakpoint injection.
|
|
110
|
-
const response = (await this._client.responses.create(params, { signal: request.signal }));
|
|
111
|
-
return this._normalize(response, request.model);
|
|
112
208
|
}
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// LlmTransport — stream()
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
113
212
|
/**
|
|
114
|
-
* Stream a completion using the
|
|
213
|
+
* Stream a completion using the Responses API SSE stream.
|
|
115
214
|
*
|
|
116
215
|
* Yields {@link NormalizedDelta} chunks from `response.output_text.delta`
|
|
117
|
-
* events
|
|
118
|
-
*
|
|
216
|
+
* events parsed from the raw SSE stream. The final delta carries
|
|
217
|
+
* `stopReason` and `usage` from `response.completed`.
|
|
218
|
+
*
|
|
219
|
+
* Wire shape:
|
|
220
|
+
* - `store: false` (required — codex backend rejects absent or true)
|
|
221
|
+
* - `stream: true`
|
|
222
|
+
* - `Accept: text/event-stream`
|
|
223
|
+
* - `OpenAI-Beta: responses=experimental`
|
|
119
224
|
*
|
|
120
225
|
* @param request - Provider-neutral request parameters.
|
|
121
226
|
* @param _ctx - Transport context (unused; `request.signal` handles abort).
|
|
122
227
|
* @returns Async iterable of normalized delta chunks.
|
|
123
228
|
*/
|
|
124
229
|
async *stream(request, _ctx) {
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// — usage arrives in the response.completed event, not a dedicated usage chunk.
|
|
140
|
-
const responseStream = (await this._client.responses.create(params, { signal: request.signal }));
|
|
230
|
+
const body = this._buildBody(request, true);
|
|
231
|
+
const headers = this._buildHeaders(true);
|
|
232
|
+
const res = await fetch(this._endpointUrl, {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
headers,
|
|
235
|
+
body: JSON.stringify(body),
|
|
236
|
+
signal: request.signal,
|
|
237
|
+
});
|
|
238
|
+
if (!res.ok) {
|
|
239
|
+
throw new Error(await buildHttpError(res));
|
|
240
|
+
}
|
|
241
|
+
if (!res.body) {
|
|
242
|
+
throw new Error('codex_responses: response body is null (no SSE stream)');
|
|
243
|
+
}
|
|
141
244
|
let finishReason = null;
|
|
142
245
|
let finalUsage = null;
|
|
143
|
-
for await (const event of
|
|
144
|
-
const
|
|
145
|
-
const evType = ev['type'];
|
|
246
|
+
for await (const event of parseSSE(res.body, request.signal)) {
|
|
247
|
+
const evType = event['type'];
|
|
146
248
|
if (evType === 'response.output_text.delta') {
|
|
147
|
-
const delta =
|
|
249
|
+
const delta = event['delta'];
|
|
148
250
|
if (delta) {
|
|
149
251
|
yield { text: delta, reasoning: '', stopReason: null, usage: null };
|
|
150
252
|
}
|
|
151
253
|
continue;
|
|
152
254
|
}
|
|
153
|
-
// PARITY GAP: reasoning_summary_text.delta not mapped to delta.reasoning
|
|
154
|
-
// because the Responses API streams reasoning summary separately.
|
|
155
|
-
// Once the API stabilizes reasoning streaming, wire this through.
|
|
156
255
|
if (evType === 'response.completed') {
|
|
157
|
-
const completedResponse =
|
|
256
|
+
const completedResponse = event['response'];
|
|
158
257
|
if (completedResponse) {
|
|
159
|
-
finishReason = completedResponse
|
|
160
|
-
const usage = completedResponse
|
|
258
|
+
finishReason = completedResponse['status'] ?? 'completed';
|
|
259
|
+
const usage = completedResponse['usage'];
|
|
161
260
|
if (usage) {
|
|
162
|
-
finalUsage =
|
|
261
|
+
finalUsage = normalizeUsage(usage);
|
|
163
262
|
}
|
|
164
263
|
}
|
|
165
264
|
break;
|
|
166
265
|
}
|
|
167
266
|
if (evType === 'response.failed' || evType === 'response.incomplete') {
|
|
168
|
-
const failedResponse =
|
|
267
|
+
const failedResponse = event['response'];
|
|
169
268
|
if (failedResponse) {
|
|
170
|
-
finishReason = failedResponse
|
|
171
|
-
const usage = failedResponse
|
|
269
|
+
finishReason = failedResponse['status'] ?? evType;
|
|
270
|
+
const usage = failedResponse['usage'];
|
|
172
271
|
if (usage) {
|
|
173
|
-
finalUsage =
|
|
272
|
+
finalUsage = normalizeUsage(usage);
|
|
174
273
|
}
|
|
175
274
|
}
|
|
176
275
|
break;
|
|
177
276
|
}
|
|
277
|
+
if (evType === 'error') {
|
|
278
|
+
const code = event['code'];
|
|
279
|
+
const message = event['message'];
|
|
280
|
+
throw new Error(`Codex error: ${message ?? code ?? JSON.stringify(event)}`);
|
|
281
|
+
}
|
|
178
282
|
}
|
|
179
283
|
yield {
|
|
180
284
|
text: '',
|
|
@@ -184,146 +288,191 @@ export class CodexResponsesTransport {
|
|
|
184
288
|
};
|
|
185
289
|
}
|
|
186
290
|
// ---------------------------------------------------------------------------
|
|
187
|
-
//
|
|
291
|
+
// Private helpers
|
|
188
292
|
// ---------------------------------------------------------------------------
|
|
189
293
|
/**
|
|
190
|
-
*
|
|
294
|
+
* Build the HTTP headers for a Codex request.
|
|
191
295
|
*
|
|
192
|
-
*
|
|
193
|
-
* -
|
|
194
|
-
* - `
|
|
195
|
-
*
|
|
196
|
-
* - `
|
|
197
|
-
* `toolUseId` as `call_id`.
|
|
296
|
+
* Merges the auth/cloudflare headers from `_defaultHeaders` with the
|
|
297
|
+
* mandatory Codex-specific headers:
|
|
298
|
+
* - `OpenAI-Beta: responses=experimental`
|
|
299
|
+
* - `content-type: application/json`
|
|
300
|
+
* - `accept: text/event-stream` (always — both complete() and stream() use SSE)
|
|
198
301
|
*
|
|
199
|
-
*
|
|
200
|
-
* `
|
|
201
|
-
* call round-trip), the item is converted to a `function_call` input item
|
|
202
|
-
* so the Responses API can continue the conversation.
|
|
302
|
+
* The Authorization header is injected only if not already present in
|
|
303
|
+
* `_defaultHeaders` (OAuth callers pre-set it via buildCodexOAuthHeaders).
|
|
203
304
|
*
|
|
204
|
-
* @param
|
|
205
|
-
* @returns
|
|
305
|
+
* @param streaming - Whether this is a streaming (SSE) or completion call.
|
|
306
|
+
* @returns Plain string record of all request headers.
|
|
206
307
|
*/
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
// user or assistant messages
|
|
220
|
-
const contentItems = buildContentList(msg);
|
|
221
|
-
if (contentItems.length === 1 && contentItems[0]?.['type'] === 'input_text') {
|
|
222
|
-
// Simple text — use the EasyInputMessage shorthand
|
|
223
|
-
items.push({
|
|
224
|
-
type: 'message',
|
|
225
|
-
role: msg.role === 'assistant' ? 'assistant' : 'user',
|
|
226
|
-
content: contentItems[0]['text'],
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
items.push({
|
|
231
|
-
type: 'message',
|
|
232
|
-
role: msg.role === 'assistant' ? 'assistant' : 'user',
|
|
233
|
-
content: contentItems,
|
|
234
|
-
});
|
|
235
|
-
}
|
|
308
|
+
_buildHeaders(streaming) {
|
|
309
|
+
const headers = {
|
|
310
|
+
...this._defaultHeaders,
|
|
311
|
+
'OpenAI-Beta': 'responses=experimental',
|
|
312
|
+
'content-type': 'application/json',
|
|
313
|
+
accept: streaming ? 'text/event-stream' : 'application/json',
|
|
314
|
+
};
|
|
315
|
+
// Inject Authorization only when callers haven't pre-set it.
|
|
316
|
+
const hasAuth = Object.keys(headers).some((k) => k.toLowerCase() === 'authorization');
|
|
317
|
+
if (!hasAuth) {
|
|
318
|
+
headers['Authorization'] = `Bearer ${this._apiKey}`;
|
|
236
319
|
}
|
|
237
|
-
return
|
|
238
|
-
}
|
|
239
|
-
// ---------------------------------------------------------------------------
|
|
240
|
-
// Tool conversion
|
|
241
|
-
// ---------------------------------------------------------------------------
|
|
242
|
-
/**
|
|
243
|
-
* Convert provider-neutral tool definitions to the Responses API
|
|
244
|
-
* `{type: 'function', name, description, parameters, strict}` shape.
|
|
245
|
-
*
|
|
246
|
-
* @param tools - Provider-neutral tool definitions (may be undefined).
|
|
247
|
-
* @returns Responses API tool objects, or empty array.
|
|
248
|
-
*/
|
|
249
|
-
_buildTools(tools) {
|
|
250
|
-
if (!tools || tools.length === 0)
|
|
251
|
-
return [];
|
|
252
|
-
return tools.map((t) => ({
|
|
253
|
-
type: 'function',
|
|
254
|
-
name: t.name,
|
|
255
|
-
description: t.description,
|
|
256
|
-
parameters: t.inputSchema,
|
|
257
|
-
strict: false,
|
|
258
|
-
}));
|
|
320
|
+
return headers;
|
|
259
321
|
}
|
|
260
|
-
// ---------------------------------------------------------------------------
|
|
261
|
-
// Response normalization
|
|
262
|
-
// ---------------------------------------------------------------------------
|
|
263
322
|
/**
|
|
264
|
-
*
|
|
265
|
-
* {@link NormalizedResponse}.
|
|
323
|
+
* Build the JSON request body for a Codex Responses API call.
|
|
266
324
|
*
|
|
267
|
-
*
|
|
268
|
-
* - `
|
|
269
|
-
*
|
|
270
|
-
* - `
|
|
271
|
-
* - `stopReason` — from `response.status` (completed / failed / incomplete).
|
|
272
|
-
* - `usage` — `input_tokens` → `inputTokens`, `output_tokens` → `outputTokens`.
|
|
273
|
-
* - `cachedTokens` — from `usage.input_tokens_details.cached_tokens`.
|
|
274
|
-
* - `raw` — unmodified SDK response.
|
|
275
|
-
*
|
|
276
|
-
* @param response - Raw Responses API response object.
|
|
277
|
-
* @param requestedModel - Model string from the originating request.
|
|
278
|
-
* @returns Normalized response envelope.
|
|
279
|
-
*/
|
|
280
|
-
_normalize(response, requestedModel) {
|
|
281
|
-
const content = response.output_text && response.output_text.length > 0
|
|
282
|
-
? response.output_text
|
|
283
|
-
: extractTextFromOutput(response.output);
|
|
284
|
-
const toolCalls = extractToolCallsFromOutput(response.output);
|
|
285
|
-
const usage = this._normalizeUsage(response.usage ?? null);
|
|
286
|
-
return {
|
|
287
|
-
id: response.id ?? `resp-${Date.now().toString(36)}`,
|
|
288
|
-
model: String(response.model ?? requestedModel),
|
|
289
|
-
content: content || null,
|
|
290
|
-
toolCalls: toolCalls.length > 0 ? toolCalls : null,
|
|
291
|
-
stopReason: response.status ?? 'completed',
|
|
292
|
-
usage,
|
|
293
|
-
providerData: {
|
|
294
|
-
codex_response_id: response.id,
|
|
295
|
-
},
|
|
296
|
-
raw: response,
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Normalize OpenAI Responses API usage into {@link NormalizedUsage}.
|
|
325
|
+
* Mandatory fields per the Codex backend:
|
|
326
|
+
* - `store: false` — backend rejects absent or `true`.
|
|
327
|
+
* - `instructions` — system prompt (defaulting to empty string when absent).
|
|
328
|
+
* - `input` — array of Responses API input items.
|
|
301
329
|
*
|
|
302
|
-
* @param
|
|
303
|
-
* @
|
|
330
|
+
* @param request - Provider-neutral request.
|
|
331
|
+
* @param streaming - Whether to enable SSE streaming.
|
|
332
|
+
* @returns Serialisable request body.
|
|
304
333
|
*/
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
334
|
+
_buildBody(request, streaming) {
|
|
335
|
+
const body = {
|
|
336
|
+
model: request.model,
|
|
337
|
+
input: buildInputItems(request),
|
|
338
|
+
instructions: request.system ?? '',
|
|
339
|
+
store: false,
|
|
340
|
+
stream: streaming,
|
|
311
341
|
};
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
342
|
+
// NOTE: the Codex ChatGPT backend rejects `max_output_tokens` with a 400
|
|
343
|
+
// ("Unsupported parameter: max_output_tokens"). pi-ai 0.78.x omits it too.
|
|
344
|
+
// Only set temperature when explicitly provided by the caller.
|
|
345
|
+
if (request.temperature != null)
|
|
346
|
+
body.temperature = request.temperature;
|
|
347
|
+
const tools = buildTools(request.tools);
|
|
348
|
+
if (tools.length > 0)
|
|
349
|
+
body.tools = tools;
|
|
350
|
+
return body;
|
|
317
351
|
}
|
|
318
352
|
}
|
|
319
353
|
// ---------------------------------------------------------------------------
|
|
320
354
|
// Module-private helpers
|
|
321
355
|
// ---------------------------------------------------------------------------
|
|
322
356
|
/**
|
|
323
|
-
*
|
|
357
|
+
* Resolve the full Responses API endpoint URL from an optional base URL.
|
|
358
|
+
*
|
|
359
|
+
* Mirrors the `resolveCodexUrl` logic from pi-ai 0.78.x so the same base URL
|
|
360
|
+
* variants work:
|
|
361
|
+
* - `https://chatgpt.com/backend-api` → `.../codex/responses`
|
|
362
|
+
* - `https://chatgpt.com/backend-api/codex` → `.../codex/responses`
|
|
363
|
+
* - `https://chatgpt.com/backend-api/codex/responses` → unchanged
|
|
364
|
+
* - `undefined` (no override) → ChatGPT Codex default
|
|
365
|
+
*
|
|
366
|
+
* For non-ChatGPT providers (xAI, custom shims) the raw baseUrl is
|
|
367
|
+
* appended with `/responses` when it doesn't already end with `/responses`.
|
|
368
|
+
*
|
|
369
|
+
* @param baseUrl - Optional override base URL (no trailing slash).
|
|
370
|
+
* @returns The fully resolved endpoint URL.
|
|
371
|
+
*/
|
|
372
|
+
export function resolveCodexUrl(baseUrl) {
|
|
373
|
+
const DEFAULT_BASE = 'https://chatgpt.com/backend-api';
|
|
374
|
+
const raw = baseUrl?.trim() ? baseUrl.trim() : DEFAULT_BASE;
|
|
375
|
+
const normalized = raw.replace(/\/+$/, '');
|
|
376
|
+
if (normalized.endsWith('/codex/responses'))
|
|
377
|
+
return normalized;
|
|
378
|
+
if (normalized.endsWith('/codex'))
|
|
379
|
+
return `${normalized}/responses`;
|
|
380
|
+
return `${normalized}/codex/responses`;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Consume an HTTP error response and build a descriptive error message.
|
|
384
|
+
*
|
|
385
|
+
* Reads the response body (if present) and attempts to extract a
|
|
386
|
+
* `{error.message}` payload from JSON. Falls back to the raw body text when
|
|
387
|
+
* JSON parsing fails, or to the HTTP status text when the body is empty.
|
|
388
|
+
*
|
|
389
|
+
* This replaces the previous "(no body)" message that appeared whenever the
|
|
390
|
+
* Codex backend returned a structured error (e.g. `{"error":{"message":"…"}}`).
|
|
391
|
+
*
|
|
392
|
+
* @param res - The failed fetch Response.
|
|
393
|
+
* @returns A descriptive error string including status code and body detail.
|
|
394
|
+
*/
|
|
395
|
+
async function buildHttpError(res) {
|
|
396
|
+
let bodyDetail = '';
|
|
397
|
+
try {
|
|
398
|
+
const text = await res.text();
|
|
399
|
+
if (text) {
|
|
400
|
+
try {
|
|
401
|
+
const parsed = JSON.parse(text);
|
|
402
|
+
const err = parsed['error'];
|
|
403
|
+
bodyDetail = err?.['message'] ?? text;
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
bodyDetail = text;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
// Body read failed — fall through to status-only message.
|
|
412
|
+
}
|
|
413
|
+
return bodyDetail ? `${res.status} ${bodyDetail}` : `${res.status} status code (no body)`;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Convert provider-neutral messages to the Responses API `input` array.
|
|
417
|
+
*
|
|
418
|
+
* Mapping rules:
|
|
419
|
+
* - `user` / `assistant` string messages → `{type:'message', role, content}`.
|
|
420
|
+
* - `user` / `assistant` multimodal blocks → content list with `input_text` /
|
|
421
|
+
* `input_image` items.
|
|
422
|
+
* - `tool` messages (role=`'tool'`) → `function_call_output` items.
|
|
423
|
+
*
|
|
424
|
+
* @param request - Full transport request.
|
|
425
|
+
* @returns Responses API input items array.
|
|
426
|
+
*/
|
|
427
|
+
function buildInputItems(request) {
|
|
428
|
+
const items = [];
|
|
429
|
+
for (const msg of request.messages) {
|
|
430
|
+
if (msg.role === 'tool') {
|
|
431
|
+
items.push({
|
|
432
|
+
type: 'function_call_output',
|
|
433
|
+
call_id: msg.toolUseId ?? `call_${Date.now().toString(36)}`,
|
|
434
|
+
output: extractTextContent(msg),
|
|
435
|
+
});
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const contentItems = buildContentList(msg);
|
|
439
|
+
if (contentItems.length === 1 && contentItems[0]?.['type'] === 'input_text') {
|
|
440
|
+
items.push({
|
|
441
|
+
type: 'message',
|
|
442
|
+
role: msg.role === 'assistant' ? 'assistant' : 'user',
|
|
443
|
+
content: contentItems[0]['text'],
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
items.push({
|
|
448
|
+
type: 'message',
|
|
449
|
+
role: msg.role === 'assistant' ? 'assistant' : 'user',
|
|
450
|
+
content: contentItems,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return items;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Convert provider-neutral tool definitions to the Responses API
|
|
458
|
+
* `{type: 'function', name, description, parameters, strict}` shape.
|
|
324
459
|
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
460
|
+
* @param tools - Provider-neutral tool definitions (may be undefined).
|
|
461
|
+
* @returns Responses API tool objects, or empty array.
|
|
462
|
+
*/
|
|
463
|
+
function buildTools(tools) {
|
|
464
|
+
if (!tools || tools.length === 0)
|
|
465
|
+
return [];
|
|
466
|
+
return tools.map((t) => ({
|
|
467
|
+
type: 'function',
|
|
468
|
+
name: t.name,
|
|
469
|
+
description: t.description,
|
|
470
|
+
parameters: t.inputSchema,
|
|
471
|
+
strict: false,
|
|
472
|
+
}));
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Extract plain text content from a {@link TransportMessage}.
|
|
327
476
|
*
|
|
328
477
|
* @param message - Transport message to extract text from.
|
|
329
478
|
* @returns Plain text string.
|
|
@@ -339,9 +488,6 @@ function extractTextContent(message) {
|
|
|
339
488
|
/**
|
|
340
489
|
* Build a Responses API content list from a {@link TransportMessage}.
|
|
341
490
|
*
|
|
342
|
-
* Each text block becomes `{type: 'input_text', text}`.
|
|
343
|
-
* Each image block becomes `{type: 'input_image', image_url, detail: 'auto'}`.
|
|
344
|
-
*
|
|
345
491
|
* @param message - Transport message to convert.
|
|
346
492
|
* @returns Array of Responses API content items.
|
|
347
493
|
*/
|
|
@@ -353,67 +499,119 @@ function buildContentList(message) {
|
|
|
353
499
|
if (block.type === 'text') {
|
|
354
500
|
return { type: 'input_text', text: block.text };
|
|
355
501
|
}
|
|
356
|
-
// image block
|
|
357
502
|
const source = block.source;
|
|
358
503
|
const imageUrl = source.type === 'base64' ? `data:${source.mediaType};base64,${source.data}` : source.data;
|
|
359
504
|
return { type: 'input_image', image_url: imageUrl, detail: 'auto' };
|
|
360
505
|
});
|
|
361
506
|
}
|
|
362
|
-
/**
|
|
363
|
-
* Extract plain text from a Responses API `output` item array.
|
|
364
|
-
*
|
|
365
|
-
* Scans for `{type: 'message'}` output items and concatenates all
|
|
366
|
-
* `{type: 'output_text'}` content parts.
|
|
367
|
-
*
|
|
368
|
-
* @param output - Array of ResponseOutputItem objects (typed as unknown[]).
|
|
369
|
-
* @returns Concatenated text content, or empty string.
|
|
370
|
-
*/
|
|
371
|
-
function extractTextFromOutput(output) {
|
|
372
|
-
const parts = [];
|
|
373
|
-
for (const item of output) {
|
|
374
|
-
const it = item;
|
|
375
|
-
if (it['type'] !== 'message')
|
|
376
|
-
continue;
|
|
377
|
-
const content = it['content'];
|
|
378
|
-
if (!Array.isArray(content))
|
|
379
|
-
continue;
|
|
380
|
-
for (const part of content) {
|
|
381
|
-
if (part['type'] === 'output_text' && typeof part['text'] === 'string') {
|
|
382
|
-
parts.push(part['text']);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
return parts.join('');
|
|
387
|
-
}
|
|
388
507
|
/**
|
|
389
508
|
* Extract tool calls from a Responses API `output` item array.
|
|
390
509
|
*
|
|
391
|
-
*
|
|
392
|
-
* {@link NormalizedToolCall}.
|
|
393
|
-
*
|
|
394
|
-
* The Responses API uses `call_id` as the stable tool-call identifier (not `id`).
|
|
395
|
-
* Both are stored: `NormalizedToolCall.id` is set to `call_id` (the replay key),
|
|
396
|
-
* and `providerData.response_item_id` carries the item's `id` field for tracing.
|
|
397
|
-
*
|
|
398
|
-
* @param output - Array of ResponseOutputItem objects (typed as unknown[]).
|
|
510
|
+
* @param output - Array of ResponseOutputItem objects.
|
|
399
511
|
* @returns Array of normalized tool calls.
|
|
400
512
|
*/
|
|
401
513
|
function extractToolCallsFromOutput(output) {
|
|
402
514
|
const calls = [];
|
|
403
515
|
for (const item of output) {
|
|
404
|
-
|
|
405
|
-
if (it['type'] !== 'function_call')
|
|
516
|
+
if (item['type'] !== 'function_call')
|
|
406
517
|
continue;
|
|
407
518
|
calls.push({
|
|
408
|
-
id: typeof
|
|
409
|
-
name: typeof
|
|
410
|
-
arguments: typeof
|
|
519
|
+
id: typeof item['call_id'] === 'string' ? item['call_id'] : null,
|
|
520
|
+
name: typeof item['name'] === 'string' ? item['name'] : 'unknown',
|
|
521
|
+
arguments: typeof item['arguments'] === 'string' ? item['arguments'] : '{}',
|
|
411
522
|
providerData: {
|
|
412
|
-
call_id:
|
|
413
|
-
response_item_id:
|
|
523
|
+
call_id: item['call_id'],
|
|
524
|
+
response_item_id: item['id'],
|
|
414
525
|
},
|
|
415
526
|
});
|
|
416
527
|
}
|
|
417
528
|
return calls;
|
|
418
529
|
}
|
|
530
|
+
/**
|
|
531
|
+
* Normalize raw Responses API usage into {@link NormalizedUsage}.
|
|
532
|
+
*
|
|
533
|
+
* @param usage - Raw usage object (may be empty or undefined fields).
|
|
534
|
+
* @returns Normalized usage with optional `cachedTokens`.
|
|
535
|
+
*/
|
|
536
|
+
function normalizeUsage(usage) {
|
|
537
|
+
const inputTokens = usage['input_tokens'] ?? 0;
|
|
538
|
+
const outputTokens = usage['output_tokens'] ?? 0;
|
|
539
|
+
const normalized = { inputTokens, outputTokens };
|
|
540
|
+
const details = usage['input_tokens_details'];
|
|
541
|
+
const cached = details?.['cached_tokens'];
|
|
542
|
+
if (typeof cached === 'number' && cached > 0) {
|
|
543
|
+
normalized.cachedTokens = cached;
|
|
544
|
+
}
|
|
545
|
+
return normalized;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Parse a raw SSE (Server-Sent Events) stream and yield JSON event objects.
|
|
549
|
+
*
|
|
550
|
+
* Reads chunks from a ReadableStream, splits on `\n\n` event boundaries,
|
|
551
|
+
* and parses each `data:` line as JSON. `[DONE]` terminates the loop.
|
|
552
|
+
*
|
|
553
|
+
* Mirrors the `parseSSE` function from pi-ai 0.78.x
|
|
554
|
+
* (`@earendil-works/pi-ai/dist/providers/openai-codex-responses.js`).
|
|
555
|
+
*
|
|
556
|
+
* @param body - The ReadableStream from the fetch response.
|
|
557
|
+
* @param signal - Optional AbortSignal for early termination.
|
|
558
|
+
* @returns Async iterable of parsed JSON event objects.
|
|
559
|
+
*/
|
|
560
|
+
async function* parseSSE(body, signal) {
|
|
561
|
+
const reader = body.getReader();
|
|
562
|
+
const decoder = new TextDecoder();
|
|
563
|
+
let buffer = '';
|
|
564
|
+
const onAbort = () => {
|
|
565
|
+
void reader.cancel().catch(() => undefined);
|
|
566
|
+
};
|
|
567
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
568
|
+
try {
|
|
569
|
+
while (true) {
|
|
570
|
+
if (signal?.aborted)
|
|
571
|
+
throw new Error('Request was aborted');
|
|
572
|
+
const { done, value } = await reader.read();
|
|
573
|
+
if (signal?.aborted)
|
|
574
|
+
throw new Error('Request was aborted');
|
|
575
|
+
if (done)
|
|
576
|
+
break;
|
|
577
|
+
buffer += decoder.decode(value, { stream: true });
|
|
578
|
+
let idx = buffer.indexOf('\n\n');
|
|
579
|
+
while (idx !== -1) {
|
|
580
|
+
const chunk = buffer.slice(0, idx);
|
|
581
|
+
buffer = buffer.slice(idx + 2);
|
|
582
|
+
const dataLines = chunk
|
|
583
|
+
.split('\n')
|
|
584
|
+
.filter((l) => l.startsWith('data:'))
|
|
585
|
+
.map((l) => l.slice(5).trim());
|
|
586
|
+
if (dataLines.length > 0) {
|
|
587
|
+
const data = dataLines.join('\n').trim();
|
|
588
|
+
if (data && data !== '[DONE]') {
|
|
589
|
+
try {
|
|
590
|
+
yield JSON.parse(data);
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
// Malformed SSE JSON — skip event; don't crash the stream.
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
idx = buffer.indexOf('\n\n');
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
finally {
|
|
602
|
+
signal?.removeEventListener('abort', onAbort);
|
|
603
|
+
try {
|
|
604
|
+
await reader.cancel();
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
// ignore
|
|
608
|
+
}
|
|
609
|
+
try {
|
|
610
|
+
reader.releaseLock();
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
// ignore
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
419
617
|
//# sourceMappingURL=codex-responses.js.map
|