@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.
Files changed (152) hide show
  1. package/dist/check/pr-gate.d.ts +6 -2
  2. package/dist/check/pr-gate.d.ts.map +1 -1
  3. package/dist/check/pr-gate.js +14 -32
  4. package/dist/check/pr-gate.js.map +1 -1
  5. package/dist/config/config-repair.d.ts +70 -0
  6. package/dist/config/config-repair.d.ts.map +1 -0
  7. package/dist/config/config-repair.js +248 -0
  8. package/dist/config/config-repair.js.map +1 -0
  9. package/dist/config/registry.d.ts.map +1 -1
  10. package/dist/config/registry.js +4 -4
  11. package/dist/config/registry.js.map +1 -1
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +11 -10
  14. package/dist/config.js.map +1 -1
  15. package/dist/docs/export-document.js +58 -16
  16. package/dist/docs/export-document.js.map +2 -2
  17. package/dist/gc/cleanup.d.ts +23 -8
  18. package/dist/gc/cleanup.d.ts.map +1 -1
  19. package/dist/gc/cleanup.js +287 -22
  20. package/dist/gc/cleanup.js.map +1 -1
  21. package/dist/gc/janitor.d.ts +149 -0
  22. package/dist/gc/janitor.d.ts.map +1 -0
  23. package/dist/gc/janitor.js +760 -0
  24. package/dist/gc/janitor.js.map +1 -0
  25. package/dist/internal.d.ts +1 -0
  26. package/dist/internal.d.ts.map +1 -1
  27. package/dist/internal.js +3 -0
  28. package/dist/internal.js.map +1 -1
  29. package/dist/llm/cli-ops.d.ts +31 -4
  30. package/dist/llm/cli-ops.d.ts.map +1 -1
  31. package/dist/llm/cli-ops.js +110 -26
  32. package/dist/llm/cli-ops.js.map +1 -1
  33. package/dist/llm/credential-pool.d.ts +62 -0
  34. package/dist/llm/credential-pool.d.ts.map +1 -1
  35. package/dist/llm/credential-pool.js +161 -0
  36. package/dist/llm/credential-pool.js.map +1 -1
  37. package/dist/llm/cross-provider-selector.d.ts +254 -0
  38. package/dist/llm/cross-provider-selector.d.ts.map +1 -0
  39. package/dist/llm/cross-provider-selector.js +635 -0
  40. package/dist/llm/cross-provider-selector.js.map +1 -0
  41. package/dist/llm/index.d.ts +2 -0
  42. package/dist/llm/index.d.ts.map +1 -1
  43. package/dist/llm/index.js +1 -0
  44. package/dist/llm/index.js.map +1 -1
  45. package/dist/llm/local-model-fit.d.ts +260 -0
  46. package/dist/llm/local-model-fit.d.ts.map +1 -0
  47. package/dist/llm/local-model-fit.js +563 -0
  48. package/dist/llm/local-model-fit.js.map +1 -0
  49. package/dist/llm/plugin-facade.js +883 -231
  50. package/dist/llm/plugin-facade.js.map +3 -3
  51. package/dist/llm/provider-registry/builtin/ollama.d.ts +18 -0
  52. package/dist/llm/provider-registry/builtin/ollama.d.ts.map +1 -1
  53. package/dist/llm/provider-registry/builtin/ollama.js +20 -2
  54. package/dist/llm/provider-registry/builtin/ollama.js.map +1 -1
  55. package/dist/llm/role-resolver.d.ts.map +1 -1
  56. package/dist/llm/role-resolver.js +45 -10
  57. package/dist/llm/role-resolver.js.map +1 -1
  58. package/dist/llm/transports/codex-responses.d.ts +111 -88
  59. package/dist/llm/transports/codex-responses.d.ts.map +1 -1
  60. package/dist/llm/transports/codex-responses.js +456 -258
  61. package/dist/llm/transports/codex-responses.js.map +1 -1
  62. package/dist/release/index.d.ts +2 -2
  63. package/dist/release/index.d.ts.map +1 -1
  64. package/dist/release/index.js +1 -1
  65. package/dist/release/index.js.map +1 -1
  66. package/dist/release/reconcile.d.ts +97 -0
  67. package/dist/release/reconcile.d.ts.map +1 -1
  68. package/dist/release/reconcile.js +412 -5
  69. package/dist/release/reconcile.js.map +1 -1
  70. package/dist/resources/backend.d.ts +155 -0
  71. package/dist/resources/backend.d.ts.map +1 -0
  72. package/dist/resources/backend.js +23 -0
  73. package/dist/resources/backend.js.map +1 -0
  74. package/dist/resources/linux-backend.d.ts +167 -0
  75. package/dist/resources/linux-backend.d.ts.map +1 -0
  76. package/dist/resources/linux-backend.js +259 -0
  77. package/dist/resources/linux-backend.js.map +1 -0
  78. package/dist/resources/monitor.d.ts +230 -0
  79. package/dist/resources/monitor.d.ts.map +1 -0
  80. package/dist/resources/monitor.js +255 -0
  81. package/dist/resources/monitor.js.map +1 -0
  82. package/dist/resources/spawn-wrapper.d.ts +265 -0
  83. package/dist/resources/spawn-wrapper.d.ts.map +1 -0
  84. package/dist/resources/spawn-wrapper.js +304 -0
  85. package/dist/resources/spawn-wrapper.js.map +1 -0
  86. package/dist/selfimprove/budget.d.ts +11 -2
  87. package/dist/selfimprove/budget.d.ts.map +1 -1
  88. package/dist/selfimprove/budget.js +11 -2
  89. package/dist/selfimprove/budget.js.map +1 -1
  90. package/dist/selfimprove/fix-gen-context.d.ts +172 -0
  91. package/dist/selfimprove/fix-gen-context.d.ts.map +1 -0
  92. package/dist/selfimprove/fix-gen-context.js +218 -0
  93. package/dist/selfimprove/fix-gen-context.js.map +1 -0
  94. package/dist/selfimprove/fix-gen.d.ts +285 -0
  95. package/dist/selfimprove/fix-gen.d.ts.map +1 -0
  96. package/dist/selfimprove/fix-gen.js +346 -0
  97. package/dist/selfimprove/fix-gen.js.map +1 -0
  98. package/dist/selfimprove/op-source-map.d.ts +92 -0
  99. package/dist/selfimprove/op-source-map.d.ts.map +1 -0
  100. package/dist/selfimprove/op-source-map.js +187 -0
  101. package/dist/selfimprove/op-source-map.js.map +1 -0
  102. package/dist/selfimprove/probe-helper.d.ts +61 -0
  103. package/dist/selfimprove/probe-helper.d.ts.map +1 -0
  104. package/dist/selfimprove/probe-helper.js +64 -0
  105. package/dist/selfimprove/probe-helper.js.map +1 -0
  106. package/dist/selfimprove/run-loop.d.ts +44 -4
  107. package/dist/selfimprove/run-loop.d.ts.map +1 -1
  108. package/dist/selfimprove/run-loop.js +73 -6
  109. package/dist/selfimprove/run-loop.js.map +1 -1
  110. package/dist/selfimprove/scenarios/seeded-code-regression/golden.json +18 -0
  111. package/dist/selfimprove/scenarios/seeded-code-regression/scenario.json +12 -0
  112. package/dist/sentient/cross-project-hygiene.d.ts +11 -2
  113. package/dist/sentient/cross-project-hygiene.d.ts.map +1 -1
  114. package/dist/sentient/cross-project-hygiene.js +73 -10
  115. package/dist/sentient/cross-project-hygiene.js.map +1 -1
  116. package/dist/sentient/daemon.d.ts.map +1 -1
  117. package/dist/sentient/daemon.js +7 -2
  118. package/dist/sentient/daemon.js.map +1 -1
  119. package/dist/sentient/worktree-dispatch.d.ts.map +1 -1
  120. package/dist/sentient/worktree-dispatch.js +8 -1
  121. package/dist/sentient/worktree-dispatch.js.map +1 -1
  122. package/dist/setup/index.d.ts +1 -0
  123. package/dist/setup/index.d.ts.map +1 -1
  124. package/dist/setup/index.js +1 -0
  125. package/dist/setup/index.js.map +1 -1
  126. package/dist/setup/sections/models-roles.d.ts +32 -1
  127. package/dist/setup/sections/models-roles.d.ts.map +1 -1
  128. package/dist/setup/sections/models-roles.js +118 -19
  129. package/dist/setup/sections/models-roles.js.map +1 -1
  130. package/dist/setup/whoami-summary.d.ts +34 -0
  131. package/dist/setup/whoami-summary.d.ts.map +1 -0
  132. package/dist/setup/whoami-summary.js +106 -0
  133. package/dist/setup/whoami-summary.js.map +1 -0
  134. package/dist/spawn/branch-lock.d.ts.map +1 -1
  135. package/dist/spawn/branch-lock.js +7 -1
  136. package/dist/spawn/branch-lock.js.map +1 -1
  137. package/dist/store/attachment-repair.d.ts +110 -0
  138. package/dist/store/attachment-repair.d.ts.map +1 -0
  139. package/dist/store/attachment-repair.js +326 -0
  140. package/dist/store/attachment-repair.js.map +1 -0
  141. package/dist/store/attachment-store.d.ts.map +1 -1
  142. package/dist/store/attachment-store.js +24 -7
  143. package/dist/store/attachment-store.js.map +1 -1
  144. package/dist/tasks/evidence.d.ts +34 -2
  145. package/dist/tasks/evidence.d.ts.map +1 -1
  146. package/dist/tasks/evidence.js +293 -35
  147. package/dist/tasks/evidence.js.map +1 -1
  148. package/dist/verification/verify-tools.d.ts.map +1 -1
  149. package/dist/verification/verify-tools.js +5 -6
  150. package/dist/verification/verify-tools.js.map +1 -1
  151. package/package.json +22 -12
  152. 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 providers that expose the OpenAI
5
- * Responses API (`openai.responses.create`), including:
6
- * - OpenAI (Codex CLI models, GPT-4.1, o-series)
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
- * Key behavioral differences from ChatCompletionsTransport:
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
- * Parity gaps vs ChatCompletions are documented inline with:
20
- * `// PARITY GAP: <feature> not yet supported by OpenAI Responses API`
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
- * Wraps `openai.responses.create()` and normalizes requests/responses to/from
35
- * the provider-neutral {@link LlmTransport} interface.
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
- * Input items are built by {@link _buildInputItems}, which converts
38
- * {@link TransportMessage} arrays into the Responses API `input` format.
39
- * Multi-turn tool replay is handled by injecting `function_call_output`
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.OPENAI_API_KEY!,
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
- /** Underlying OpenAI SDK client. */
68
- _client;
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 including provider, API key, and
73
- * optional base URL / default headers.
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._client = new OpenAIClient({
78
- apiKey: opts.apiKey,
79
- baseURL: opts.baseUrl,
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 OpenAI Responses API.
102
+ * Execute a single (non-streaming) completion using the Responses API.
85
103
  *
86
- * Maps the provider-neutral {@link TransportRequest} to
87
- * `openai.responses.create()` and normalizes the {@link Response} into a
88
- * {@link NormalizedResponse}.
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 input = this._buildInputItems(request);
96
- const tools = this._buildTools(request.tools);
97
- const params = {
98
- model: request.model,
99
- input,
100
- max_output_tokens: request.maxTokens,
101
- temperature: request.temperature ?? 0.7,
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 OpenAI Responses API SSE stream.
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. The final delta is emitted when `response.completed` fires and
118
- * carries `stopReason` and `usage`.
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 input = this._buildInputItems(request);
126
- const tools = this._buildTools(request.tools);
127
- const params = {
128
- model: request.model,
129
- input,
130
- max_output_tokens: request.maxTokens,
131
- temperature: request.temperature ?? 0.7,
132
- stream: true,
133
- };
134
- if (request.system)
135
- params['instructions'] = request.system;
136
- if (tools.length > 0)
137
- params['tools'] = tools;
138
- // PARITY GAP: stream_options.include_usage not available in Responses API stream
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 responseStream) {
144
- const ev = event;
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 = ev['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 = ev['response'] ?? null;
256
+ const completedResponse = event['response'];
158
257
  if (completedResponse) {
159
- finishReason = completedResponse.status ?? 'completed';
160
- const usage = completedResponse.usage;
258
+ finishReason = completedResponse['status'] ?? 'completed';
259
+ const usage = completedResponse['usage'];
161
260
  if (usage) {
162
- finalUsage = this._normalizeUsage(usage);
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 = ev['response'] ?? null;
267
+ const failedResponse = event['response'];
169
268
  if (failedResponse) {
170
- finishReason = failedResponse.status ?? evType;
171
- const usage = failedResponse.usage;
269
+ finishReason = failedResponse['status'] ?? evType;
270
+ const usage = failedResponse['usage'];
172
271
  if (usage) {
173
- finalUsage = this._normalizeUsage(usage);
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
- // Input items construction
291
+ // Private helpers
188
292
  // ---------------------------------------------------------------------------
189
293
  /**
190
- * Convert provider-neutral messages to the Responses API `input` array.
294
+ * Build the HTTP headers for a Codex request.
191
295
  *
192
- * Mapping rules:
193
- * - `user` / `assistant` string messages → `EasyInputMessage` with `type:'message'`.
194
- * - `user` / `assistant` multimodal blocks → content list with `input_text` /
195
- * `input_image` items.
196
- * - `tool` messages (role=`'tool'`) → `function_call_output` items using
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
- * Multi-turn tool replay: when an assistant message's raw content is a
200
- * `function_call` JSON envelope (injected by the agent loop after a tool
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 request - Full transport request.
205
- * @returns Responses API input items array.
305
+ * @param streaming - Whether this is a streaming (SSE) or completion call.
306
+ * @returns Plain string record of all request headers.
206
307
  */
207
- _buildInputItems(request) {
208
- const items = [];
209
- for (const msg of request.messages) {
210
- if (msg.role === 'tool') {
211
- // Tool result: convert to function_call_output
212
- items.push({
213
- type: 'function_call_output',
214
- call_id: msg.toolUseId ?? `call_${Date.now().toString(36)}`,
215
- output: typeof msg.content === 'string' ? msg.content : extractTextContent(msg),
216
- });
217
- continue;
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 items;
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
- * Normalize an OpenAI Responses API {@link Response} into a
265
- * {@link NormalizedResponse}.
323
+ * Build the JSON request body for a Codex Responses API call.
266
324
  *
267
- * Mapping rules:
268
- * - `content` — from `response.output_text` shortcut (SDK convenience
269
- * property that aggregates all output_text parts).
270
- * - `toolCalls` — from `output` items of `type: 'function_call'`.
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 usage - Raw usage object (may be null when the API omits it).
303
- * @returns Normalized usage with optional `cachedTokens`.
330
+ * @param request - Provider-neutral request.
331
+ * @param streaming - Whether to enable SSE streaming.
332
+ * @returns Serialisable request body.
304
333
  */
305
- _normalizeUsage(usage) {
306
- if (!usage)
307
- return { inputTokens: 0, outputTokens: 0 };
308
- const normalized = {
309
- inputTokens: usage.input_tokens ?? 0,
310
- outputTokens: usage.output_tokens ?? 0,
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
- const cached = usage.input_tokens_details?.cached_tokens;
313
- if (typeof cached === 'number' && cached > 0) {
314
- normalized.cachedTokens = cached;
315
- }
316
- return normalized;
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
- * Extract plain text content from a {@link TransportMessage}.
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
- * When `content` is a plain string, returns it as-is. When it is a multimodal
326
- * block array, concatenates all `text` blocks and drops `image` blocks.
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
- * Scans for `{type: 'function_call'}` items and maps them to
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
- const it = item;
405
- if (it['type'] !== 'function_call')
516
+ if (item['type'] !== 'function_call')
406
517
  continue;
407
518
  calls.push({
408
- id: typeof it['call_id'] === 'string' ? it['call_id'] : null,
409
- name: typeof it['name'] === 'string' ? it['name'] : 'unknown',
410
- arguments: typeof it['arguments'] === 'string' ? it['arguments'] : '{}',
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: it['call_id'],
413
- response_item_id: it['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