@yanzc/claude-pro 0.0.0-stage → 0.4.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.
Files changed (58) hide show
  1. package/.env.example +41 -0
  2. package/LICENSE +21 -0
  3. package/README.md +314 -3
  4. package/bin/claude-pro.js +32 -0
  5. package/package.json +40 -6
  6. package/src/agent.js +752 -0
  7. package/src/agents.js +186 -0
  8. package/src/api.js +529 -0
  9. package/src/cache.js +150 -0
  10. package/src/checkpoints.js +184 -0
  11. package/src/claude-code.js +60 -0
  12. package/src/codex/client.js +307 -0
  13. package/src/codex/translate-request.js +239 -0
  14. package/src/codex/translate-stream.js +328 -0
  15. package/src/commands.js +839 -0
  16. package/src/config.js +453 -0
  17. package/src/context.js +109 -0
  18. package/src/filestate.js +57 -0
  19. package/src/gateway.js +143 -0
  20. package/src/hooks.js +81 -0
  21. package/src/main.js +193 -0
  22. package/src/mcp.js +290 -0
  23. package/src/modes.js +154 -0
  24. package/src/oauth.js +287 -0
  25. package/src/peers.js +157 -0
  26. package/src/print.js +106 -0
  27. package/src/prompts.js +179 -0
  28. package/src/ratelimit.js +244 -0
  29. package/src/restart.js +115 -0
  30. package/src/runtime.js +162 -0
  31. package/src/selfcheck.js +71 -0
  32. package/src/session.js +139 -0
  33. package/src/supervisor.js +110 -0
  34. package/src/tasks.js +76 -0
  35. package/src/tools/bash.js +227 -0
  36. package/src/tools/effort.js +41 -0
  37. package/src/tools/files.js +352 -0
  38. package/src/tools/index.js +52 -0
  39. package/src/tools/interaction.js +157 -0
  40. package/src/tools/notebook.js +91 -0
  41. package/src/tools/peers.js +45 -0
  42. package/src/tools/restart.js +50 -0
  43. package/src/tools/search.js +458 -0
  44. package/src/tools/skill.js +32 -0
  45. package/src/tools/task.js +139 -0
  46. package/src/tools/web.js +283 -0
  47. package/src/ui/ansi.js +228 -0
  48. package/src/ui/dialogs.js +122 -0
  49. package/src/ui/editor.js +389 -0
  50. package/src/ui/markdown.js +249 -0
  51. package/src/ui/render.js +155 -0
  52. package/src/ui/repl.js +1460 -0
  53. package/src/ui/screen.js +259 -0
  54. package/src/updater.js +93 -0
  55. package/src/utils/clipboard.js +197 -0
  56. package/src/utils/diff.js +95 -0
  57. package/src/utils/paths.js +37 -0
  58. package/src/utils/shell.js +251 -0
@@ -0,0 +1,307 @@
1
+ // The local Codex (ChatGPT) subscription login as a provider: Anthropic Messages requests from the SDK are
2
+ // translated to OpenAI Responses requests for the Codex backend, and the Responses stream back to Anthropic SSE,
3
+ // so that the rest of the program (api.js, the agent loop) works unchanged. Hooked in as the SDK's `fetch`.
4
+ // The login is the one `codex login` wrote to $CODEX_HOME/auth.json; its access token is renewed with the
5
+ // refresh token stored there, and the renewed tokens are written back for the Codex CLI to use too.
6
+
7
+ import crypto from 'node:crypto';
8
+ import fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import { buildResponsesRequest, toolNameMap } from './translate-request.js';
12
+ import { ResponsesToAnthropic, parseSSE } from './translate-stream.js';
13
+
14
+ export const CODEX_RESPONSES_URL = 'https://chatgpt.com/backend-api/codex/responses';
15
+ const OAUTH_URL = 'https://auth.openai.com/oauth/token';
16
+ const OAUTH_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
17
+ const REFRESH_MARGIN_MS = 5 * 60_000;
18
+ const EFFORTS = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'];
19
+ const LOWER_EFFORT = { ultra: 'max', max: 'xhigh', xhigh: 'high', high: 'medium', minimal: 'low', none: 'low', low: 'medium' };
20
+
21
+ export const codexHome = (env = process.env) => env.CODEX_HOME || path.join(os.homedir(), '.codex');
22
+ export const codexAuthFile = (env = process.env) => path.join(codexHome(env), 'auth.json');
23
+
24
+ export function decodeJwt(token) {
25
+ try {
26
+ return JSON.parse(Buffer.from(String(token).split('.')[1], 'base64url').toString('utf8'));
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ const expiresSoon = (token) => {
33
+ const exp = decodeJwt(token)?.exp;
34
+ return !exp || exp * 1000 - Date.now() < REFRESH_MARGIN_MS;
35
+ };
36
+
37
+ function readAuth(file) {
38
+ let text;
39
+ try {
40
+ text = fs.readFileSync(file, 'utf8');
41
+ } catch (e) {
42
+ if (e.code === 'ENOENT') throw Object.assign(new Error(`No Codex login: ${file} does not exist. Run \`codex login\` (ChatGPT plan), then try again.`), { status: 401 });
43
+ throw e;
44
+ }
45
+ try {
46
+ return JSON.parse(text);
47
+ } catch (e) {
48
+ throw Object.assign(new Error(`Cannot parse ${file}: ${e.message}`), { status: 401 });
49
+ }
50
+ }
51
+
52
+ function writeAuth(file, data) {
53
+ const body = JSON.stringify(data, null, 2);
54
+ const tmp = `${file}.claude-pro-${process.pid}.tmp`;
55
+ try {
56
+ fs.writeFileSync(tmp, body, { mode: 0o600 });
57
+ fs.renameSync(tmp, file);
58
+ } catch {
59
+ try { fs.unlinkSync(tmp); } catch { /* not there */ }
60
+ fs.writeFileSync(file, body, { mode: 0o600 });
61
+ }
62
+ }
63
+
64
+ const accountIdOf = (tokens) => tokens.account_id
65
+ || decodeJwt(tokens.id_token)?.['https://api.openai.com/auth']?.chatgpt_account_id
66
+ || decodeJwt(tokens.access_token)?.['https://api.openai.com/auth']?.chatgpt_account_id
67
+ || '';
68
+
69
+ // What /status shows about the login (nothing secret).
70
+ export function codexLoginSummary(env = process.env) {
71
+ const file = codexAuthFile(env);
72
+ try {
73
+ const data = readAuth(file);
74
+ const claims = decodeJwt(data.tokens?.id_token)?.['https://api.openai.com/auth'] || {};
75
+ const email = decodeJwt(data.tokens?.id_token)?.email || null;
76
+ if (data.tokens?.access_token) return { ok: true, file, plan: claims.chatgpt_plan_type || null, email };
77
+ if (data.OPENAI_API_KEY) return { ok: true, file, plan: 'api key' };
78
+ return { ok: false, file, error: 'no tokens in it' };
79
+ } catch (e) {
80
+ return { ok: false, file, error: e.message };
81
+ }
82
+ }
83
+
84
+ let refreshing = null;
85
+
86
+ async function refresh(file, stale, fetchImpl) {
87
+ const data = readAuth(file);
88
+ const tokens = data.tokens || {};
89
+ // The Codex CLI or app may have renewed it already.
90
+ if (tokens.access_token && tokens.access_token !== stale && !expiresSoon(tokens.access_token)) return;
91
+ if (!tokens.refresh_token) throw Object.assign(new Error('The Codex login has no refresh token. Run `codex login` again.'), { status: 401 });
92
+ const res = await fetchImpl(OAUTH_URL, {
93
+ method: 'POST',
94
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
95
+ body: JSON.stringify({ client_id: OAUTH_CLIENT_ID, grant_type: 'refresh_token', refresh_token: tokens.refresh_token, scope: 'openid profile email' }),
96
+ });
97
+ const text = await res.text();
98
+ if (!res.ok) {
99
+ let reason = text.slice(0, 200);
100
+ try { const j = JSON.parse(text); reason = j.error_description || j.error?.message || j.error || reason; } catch { /* as it is */ }
101
+ throw Object.assign(new Error(`Renewing the Codex login failed (HTTP ${res.status}: ${reason}). Run \`codex login\` again.`), { status: 401 });
102
+ }
103
+ const j = JSON.parse(text);
104
+ const next = { ...data, tokens: { ...tokens } };
105
+ for (const k of ['access_token', 'id_token', 'refresh_token']) if (j[k]) next.tokens[k] = j[k];
106
+ next.last_refresh = new Date().toISOString();
107
+ writeAuth(file, next);
108
+ }
109
+
110
+ export async function codexCredentials(file, { force = false, fetchImpl = globalThis.fetch } = {}) {
111
+ const data = readAuth(file);
112
+ const tokens = data.tokens;
113
+ if (tokens?.access_token && data.auth_mode !== 'apikey') {
114
+ if (force || expiresSoon(tokens.access_token)) {
115
+ refreshing ??= refresh(file, tokens.access_token, fetchImpl).finally(() => { refreshing = null; });
116
+ await refreshing;
117
+ const fresh = readAuth(file).tokens;
118
+ return { mode: 'chatgpt', accessToken: fresh.access_token, accountId: accountIdOf(fresh) };
119
+ }
120
+ return { mode: 'chatgpt', accessToken: tokens.access_token, accountId: accountIdOf(tokens) };
121
+ }
122
+ if (data.OPENAI_API_KEY) return { mode: 'apikey', apiKey: data.OPENAI_API_KEY };
123
+ throw Object.assign(new Error(`No usable credentials in ${file}. Run \`codex login\`.`), { status: 401 });
124
+ }
125
+
126
+ // A file's parsed contents, read again only when it changes (the footer asks for the model on every frame).
127
+ const fileCache = new Map();
128
+ function readCached(file, parse, fallback) {
129
+ let st;
130
+ try { st = fs.statSync(file); } catch { return fallback; }
131
+ const hit = fileCache.get(file);
132
+ if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) return hit.value;
133
+ let value;
134
+ try { value = parse(fs.readFileSync(file, 'utf8')); } catch { value = fallback; }
135
+ fileCache.set(file, { mtimeMs: st.mtimeMs, size: st.size, value });
136
+ return value;
137
+ }
138
+
139
+ // The models the local Codex install knows (models_cache.json): efforts, context window, base instructions.
140
+ export function codexModels(env = process.env) {
141
+ return readCached(path.join(codexHome(env), 'models_cache.json'), parseModelsCache, { clientVersion: null, models: [] });
142
+ }
143
+
144
+ function parseModelsCache(text) {
145
+ {
146
+ const j = JSON.parse(text);
147
+ const list = Array.isArray(j) ? j : j.models || [];
148
+ return {
149
+ clientVersion: /^\d+\.\d+\.\d+$/.test(String(j.client_version)) ? j.client_version : null,
150
+ models: list.filter((m) => m && typeof m.slug === 'string').map((m) => ({
151
+ slug: m.slug,
152
+ hidden: /^hid/i.test(m.visibility || ''),
153
+ efforts: (m.supported_reasoning_levels || []).map((r) => (typeof r === 'string' ? r : r?.effort)).filter(Boolean),
154
+ defaultEffort: m.default_reasoning_level || null,
155
+ contextWindow: m.context_window || null,
156
+ baseInstructions: typeof m.base_instructions === 'string' ? m.base_instructions : null,
157
+ })),
158
+ };
159
+ }
160
+ }
161
+
162
+ // The model and effort set in the Codex config.toml (top-level `model` / `model_reasoning_effort`).
163
+ export function codexConfigDefaults(env = process.env) {
164
+ return readCached(path.join(codexHome(env), 'config.toml'), (text) => {
165
+ const top = text.split(/^\s*\[/m)[0];
166
+ const pick = (key) => new RegExp(`^\\s*${key}\\s*=\\s*"([^"]+)"`, 'm').exec(top)?.[1] || null;
167
+ return { model: pick('model'), effort: pick('model_reasoning_effort') };
168
+ }, { model: null, effort: null });
169
+ }
170
+
171
+ function clampEffort(effort, supported) {
172
+ if (!effort || !supported?.length || supported.includes(effort)) return effort;
173
+ const want = EFFORTS.indexOf(effort);
174
+ return supported.reduce((best, s) => (Math.abs(EFFORTS.indexOf(s) - want) < Math.abs(EFFORTS.indexOf(best) - want) ? s : best), supported[0]);
175
+ }
176
+
177
+ // The effort levels a GPT model takes, from the local Codex models cache ([] when it is not listed there):
178
+ // gpt-6.1-sol takes low … ultra, but not none or minimal.
179
+ export const codexEfforts = (modelId, env = process.env) => codexModels(env).models.find((m) => m.slug === modelId)?.efforts || [];
180
+
181
+ // The level the Codex backend gets for a chosen one: the nearest the model takes.
182
+ export const codexEffort = (modelId, effort, env = process.env) => clampEffort(effort, codexEfforts(modelId, env));
183
+
184
+ // An Anthropic-shaped error response, which the SDK turns into an APIError with this status.
185
+ function errorResponse(status, type, message, headers = {}) {
186
+ return new Response(JSON.stringify({ type: 'error', error: { type, message } }), { status, headers: { 'content-type': 'application/json', ...headers } });
187
+ }
188
+
189
+ function upstreamError(status, text, headers) {
190
+ let message = text.slice(0, 500) || `HTTP ${status}`;
191
+ let code = '';
192
+ let resetsIn = null;
193
+ try {
194
+ const j = JSON.parse(text);
195
+ message = j.error?.message || j.detail || j.message || message;
196
+ code = j.error?.type || j.error?.code || '';
197
+ resetsIn = j.error?.resets_in_seconds ?? null;
198
+ } catch { /* not JSON */ }
199
+ const extra = {};
200
+ if (resetsIn != null) extra['retry-after'] = String(resetsIn);
201
+ else if (headers.get('retry-after')) extra['retry-after'] = headers.get('retry-after');
202
+ if (status === 429 || /usage_limit|rate_limit/i.test(code)) return { status: 429, type: 'rate_limit_error', message: `Codex: ${message}`, headers: extra, upstreamMessage: message };
203
+ if (status === 401 || status === 403) return { status, type: 'authentication_error', message: `Codex login refused (${status}): ${message}. Run \`codex login\` again if this persists.`, headers: extra, upstreamMessage: message };
204
+ return { status, type: status >= 500 ? 'api_error' : 'invalid_request_error', message: `Codex: ${message}`, headers: extra, upstreamMessage: message };
205
+ }
206
+
207
+ // The SDK's fetch for the codex provider. `options.env` is the configuration's environment (CODEX_HOME).
208
+ export function createCodexFetch({ env = process.env, url = CODEX_RESPONSES_URL, fetchImpl = globalThis.fetch, sessionId = crypto.randomUUID() } = {}) {
209
+ const sticky = { instructionsMode: 'custom', removedParams: new Set() };
210
+ return async function codexFetch(input, init = {}) {
211
+ const target = String(input?.url ?? input);
212
+ const { pathname } = new URL(target);
213
+ if (!/\/v1\/messages$/.test(pathname)) return errorResponse(404, 'not_found_error', `The codex provider does not support ${pathname}`);
214
+ let anth;
215
+ try { anth = JSON.parse(typeof init.body === 'string' ? init.body : await new Response(init.body).text()); } catch { return errorResponse(400, 'invalid_request_error', 'Request body is not valid JSON'); }
216
+ const file = codexAuthFile(env);
217
+ const known = codexModels(env);
218
+ const model = String(anth.model || '').replace(/\[1m\]$/i, '');
219
+ const info = known.models.find((m) => m.slug === model);
220
+ // No effort sent ("auto", and side queries such as titles): the model's own default, as with Claude.
221
+ const wanted = anth.output_config?.effort || info?.defaultEffort || null;
222
+ const o = {
223
+ model,
224
+ effort: clampEffort(wanted, info?.efforts),
225
+ summary: 'auto',
226
+ sessionId,
227
+ instructionsMode: sticky.instructionsMode,
228
+ codexInstructions: sticky.instructionsMode === 'codex' ? info?.baseInstructions : null,
229
+ removedParams: new Set(sticky.removedParams),
230
+ dropReasoning: false,
231
+ webSearch: false,
232
+ parallelToolCalls: true,
233
+ };
234
+ const triedEfforts = new Set([o.effort]);
235
+ let refreshed = false; // a 401 renews the token once
236
+ let force = false;
237
+ let res;
238
+ for (let attempt = 0; ; attempt++) {
239
+ let creds;
240
+ try { creds = await codexCredentials(file, { force, fetchImpl }); } catch (e) { return errorResponse(e.status || 401, 'authentication_error', e.message); }
241
+ force = false;
242
+ const headers = {
243
+ 'content-type': 'application/json',
244
+ accept: 'text/event-stream',
245
+ 'openai-beta': 'responses=experimental',
246
+ originator: 'codex_cli_rs',
247
+ version: known.clientVersion || '0.162.0',
248
+ 'user-agent': `codex_cli_rs/${known.clientVersion || '0.162.0'} (${os.type()} ${os.release()}; ${os.arch()}) claude-pro`,
249
+ session_id: sessionId,
250
+ authorization: `Bearer ${creds.mode === 'chatgpt' ? creds.accessToken : creds.apiKey}`,
251
+ };
252
+ if (creds.accountId) headers['chatgpt-account-id'] = creds.accountId;
253
+ const payload = buildResponsesRequest(anth, o);
254
+ res = await fetchImpl(creds.mode === 'chatgpt' ? url : 'https://api.openai.com/v1/responses', { method: 'POST', headers, body: JSON.stringify(payload), signal: init.signal });
255
+ if (res.ok) break;
256
+ const text = await res.text();
257
+ const err = upstreamError(res.status, text, res.headers);
258
+ if (attempt >= 5) return errorResponse(err.status, err.type, err.message, err.headers);
259
+ if (res.status === 401 && !refreshed) { refreshed = force = true; continue; }
260
+ if (res.status === 400) {
261
+ const m = err.upstreamMessage;
262
+ if (/instruction/i.test(m) && o.instructionsMode !== 'codex' && info?.baseInstructions) {
263
+ o.instructionsMode = sticky.instructionsMode = 'codex';
264
+ o.codexInstructions = info.baseInstructions;
265
+ continue;
266
+ }
267
+ if (/encrypted|reasoning item|could not be (verified|decrypted)/i.test(m) && !o.dropReasoning) { o.dropReasoning = true; continue; }
268
+ const param = m.match(/unsupported parameter:?\s*['"`]?([\w.]+)/i)?.[1];
269
+ if (param && !o.removedParams.has(param) && !['model', 'input', 'stream'].includes(param)) {
270
+ o.removedParams.add(param);
271
+ sticky.removedParams.add(param);
272
+ continue;
273
+ }
274
+ const lower = LOWER_EFFORT[o.effort];
275
+ if (/effort/i.test(m) && lower && !triedEfforts.has(lower)) { o.effort = lower; triedEfforts.add(lower); continue; }
276
+ }
277
+ return errorResponse(err.status, err.type, err.message, err.headers);
278
+ }
279
+
280
+ const toolNames = toolNameMap(anth);
281
+ const encoder = new TextEncoder();
282
+ const body = new ReadableStream({
283
+ async start(controller) {
284
+ const conv = new ResponsesToAnthropic({
285
+ model: anth.model,
286
+ toolNames,
287
+ emitThinking: true,
288
+ onEvent: (name, data) => controller.enqueue(encoder.encode(`event: ${name}\ndata: ${JSON.stringify(data)}\n\n`)),
289
+ });
290
+ conv.start();
291
+ try {
292
+ for await (const ev of parseSSE(res.body)) {
293
+ conv.handle(ev);
294
+ if (conv.done) break;
295
+ }
296
+ conv.end();
297
+ } catch (e) {
298
+ if (init.signal?.aborted) { controller.error(e); return; }
299
+ conv.fail(`Codex stream error: ${e.message}`);
300
+ }
301
+ controller.close();
302
+ },
303
+ cancel() { res.body?.cancel?.().catch(() => {}); },
304
+ });
305
+ return new Response(body, { status: 200, headers: { 'content-type': 'text/event-stream; charset=utf-8' } });
306
+ };
307
+ }
@@ -0,0 +1,239 @@
1
+ // Copied from codex-gateway (src/translate-request.mjs): Anthropic Messages request -> OpenAI Responses request.
2
+ // Anthropic Messages request -> OpenAI Responses request (Codex backend flavour).
3
+ import crypto from "node:crypto";
4
+
5
+ // Thinking blocks we emit carry Codex's encrypted reasoning in `signature`, prefixed with this
6
+ // marker, so the reasoning can be replayed on the next turn (store=false has no server state).
7
+ export const SIGNATURE_PREFIX = "cgw1:";
8
+
9
+ const hash = (s, n = 8) => crypto.createHash("sha256").update(s).digest("hex").slice(0, n);
10
+
11
+ /** OpenAI function names: ^[a-zA-Z0-9_-]{1,64}$. Long MCP names get a stable hashed suffix. */
12
+ export function upstreamToolName(name) {
13
+ let n = String(name).replace(/[^a-zA-Z0-9_-]/g, "_");
14
+ if (n.length > 64) n = `${n.slice(0, 55)}_${hash(name)}`;
15
+ return n || "tool";
16
+ }
17
+
18
+ export function systemText(system) {
19
+ if (!system) return "";
20
+ if (typeof system === "string") return system;
21
+ return system
22
+ .filter((b) => b?.type === "text" && b.text && !b.text.startsWith("x-anthropic-billing-header"))
23
+ .map((b) => b.text)
24
+ .join("\n\n");
25
+ }
26
+
27
+ const asBlocks = (content) => (typeof content === "string" ? [{ type: "text", text: content }] : content || []);
28
+
29
+ function imagePart(block) {
30
+ const s = block.source || {};
31
+ if (s.type === "base64") return { type: "input_image", image_url: `data:${s.media_type};base64,${s.data}` };
32
+ if (s.type === "url") return { type: "input_image", image_url: s.url };
33
+ return null;
34
+ }
35
+
36
+ function documentPart(block) {
37
+ const s = block.source || {};
38
+ if (s.type === "text") return { type: "input_text", text: s.data || "" };
39
+ if (s.type === "base64") {
40
+ return {
41
+ type: "input_file",
42
+ filename: block.title || (s.media_type === "application/pdf" ? "document.pdf" : "document"),
43
+ file_data: `data:${s.media_type};base64,${s.data}`,
44
+ };
45
+ }
46
+ if (s.type === "content") return { type: "input_text", text: asBlocks(s.content).map((b) => b.text || "").join("\n") };
47
+ return null;
48
+ }
49
+
50
+ function toolResultItem(block) {
51
+ const parts = asBlocks(block.content);
52
+ const errorPrefix = (text) => (block.is_error && !/^\s*<tool_use_error>/.test(text) ? `ERROR: ${text}` : text);
53
+ let output;
54
+ if (parts.some((p) => p.type === "image")) {
55
+ output = parts
56
+ .map((p) => (p.type === "image" ? imagePart(p) : p.type === "text" ? { type: "input_text", text: p.text } : null))
57
+ .filter(Boolean);
58
+ if (block.is_error) output.unshift({ type: "input_text", text: "ERROR:" });
59
+ } else {
60
+ const text = (p) =>
61
+ p.type === "text" ? p.text
62
+ : p.type === "document" ? documentPart(p)?.text || ""
63
+ : p.type === "tool_reference" ? `Tool ${p.tool_name} is now available.`
64
+ : "";
65
+ output = errorPrefix(parts.map(text).join("\n"));
66
+ }
67
+ return { type: "function_call_output", call_id: block.tool_use_id, output };
68
+ }
69
+
70
+ function reasoningItem(block) {
71
+ const sig = block.signature;
72
+ if (typeof sig !== "string" || !sig.startsWith(SIGNATURE_PREFIX)) return null;
73
+ return { type: "reasoning", summary: [], encrypted_content: sig.slice(SIGNATURE_PREFIX.length) };
74
+ }
75
+
76
+ function convertMessages(messages, { dropReasoning }) {
77
+ const input = [];
78
+ for (const msg of messages || []) {
79
+ const blocks = asBlocks(msg.content);
80
+ if (msg.role === "assistant") {
81
+ let parts = [];
82
+ const flush = () => {
83
+ if (parts.length) input.push({ type: "message", role: "assistant", content: parts });
84
+ parts = [];
85
+ };
86
+ for (const b of blocks) {
87
+ if (b.type === "text") {
88
+ if (b.text) parts.push({ type: "output_text", text: b.text });
89
+ } else if (b.type === "thinking") {
90
+ const r = !dropReasoning && reasoningItem(b);
91
+ if (r) {
92
+ flush();
93
+ input.push(r);
94
+ }
95
+ } else if (b.type === "tool_use") {
96
+ flush();
97
+ input.push({
98
+ type: "function_call",
99
+ call_id: b.id,
100
+ name: upstreamToolName(b.name),
101
+ arguments: JSON.stringify(b.input ?? {}),
102
+ });
103
+ }
104
+ }
105
+ flush();
106
+ } else {
107
+ let parts = [];
108
+ const flush = () => {
109
+ if (parts.length) input.push({ type: "message", role: "user", content: parts });
110
+ parts = [];
111
+ };
112
+ for (const b of blocks) {
113
+ if (b.type === "text") {
114
+ if (b.text) parts.push({ type: "input_text", text: b.text });
115
+ } else if (b.type === "image") {
116
+ const p = imagePart(b);
117
+ if (p) parts.push(p);
118
+ } else if (b.type === "document") {
119
+ const p = documentPart(b);
120
+ if (p) parts.push(p);
121
+ } else if (b.type === "tool_result") {
122
+ flush();
123
+ input.push(toolResultItem(b));
124
+ }
125
+ }
126
+ flush();
127
+ }
128
+ }
129
+ return input;
130
+ }
131
+
132
+ function sanitizeSchema(schema) {
133
+ const s = schema && typeof schema === "object" ? { ...schema } : {};
134
+ delete s.$schema;
135
+ if (s.type !== "object") s.type = "object";
136
+ if (!s.properties || typeof s.properties !== "object") s.properties = {};
137
+ return s;
138
+ }
139
+
140
+ function convertTools(tools, { webSearch }) {
141
+ const out = [];
142
+ for (const t of tools || []) {
143
+ if (typeof t.type === "string" && t.type.startsWith("web_search")) {
144
+ if (webSearch) out.push({ type: "web_search" });
145
+ continue;
146
+ }
147
+ if (t.type && t.type !== "custom" && !t.input_schema) continue; // other Anthropic server tools
148
+ out.push({
149
+ type: "function",
150
+ name: upstreamToolName(t.name),
151
+ description: t.description || "",
152
+ parameters: sanitizeSchema(t.input_schema),
153
+ strict: false,
154
+ });
155
+ }
156
+ return out;
157
+ }
158
+
159
+ function convertToolChoice(tc) {
160
+ if (!tc) return "auto";
161
+ switch (tc.type) {
162
+ case "any":
163
+ return "required";
164
+ case "none":
165
+ return "none";
166
+ case "tool":
167
+ return { type: "function", name: upstreamToolName(tc.name) };
168
+ default:
169
+ return "auto";
170
+ }
171
+ }
172
+
173
+ /** Maps upstream (possibly shortened) tool names back to the client's names. */
174
+ export function toolNameMap(anth) {
175
+ const map = new Map();
176
+ const add = (n) => n && map.set(upstreamToolName(n), n);
177
+ for (const t of anth.tools || []) add(t.name);
178
+ for (const m of anth.messages || []) for (const b of asBlocks(m.content)) if (b.type === "tool_use") add(b.name);
179
+ return map;
180
+ }
181
+
182
+ /** Stable per-conversation id, used as session_id header and prompt_cache_key. */
183
+ export function sessionIdFor(anth) {
184
+ const uid = anth.metadata?.user_id;
185
+ let sid = null;
186
+ if (typeof uid === "string") {
187
+ try {
188
+ sid = JSON.parse(uid).session_id || null;
189
+ } catch {}
190
+ sid ||= uid.match(/session_([0-9a-fA-F-]{36})/)?.[1] || null;
191
+ }
192
+ const seed = `${sid || "anon"}|${systemText(anth.system).slice(0, 300)}`;
193
+ const h = crypto.createHash("sha256").update(seed).digest("hex");
194
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-a${h.slice(17, 20)}-${h.slice(20, 32)}`;
195
+ }
196
+
197
+ /**
198
+ * @param anth Anthropic request body
199
+ * @param o { model, effort, summary, sessionId, instructionsMode: "custom"|"codex", codexInstructions,
200
+ * dropReasoning, removedParams:Set<string>, webSearch, parallelToolCalls }
201
+ */
202
+ export function buildResponsesRequest(anth, o) {
203
+ const system = systemText(anth.system);
204
+ const input = convertMessages(anth.messages, o);
205
+ let instructions = system || "You are a helpful assistant.";
206
+ if (o.instructionsMode === "codex" && o.codexInstructions) {
207
+ instructions = o.codexInstructions;
208
+ const note =
209
+ "The instructions above describe the Codex CLI harness. In this session you are driven by a different " +
210
+ "harness: follow the developer instructions below and use only the tools defined in this request.";
211
+ input.unshift({ type: "message", role: "developer", content: [{ type: "input_text", text: `${note}\n\n${system}` }] });
212
+ }
213
+
214
+ const payload = {
215
+ model: o.model,
216
+ instructions,
217
+ input,
218
+ store: false,
219
+ stream: true,
220
+ include: ["reasoning.encrypted_content"],
221
+ prompt_cache_key: o.sessionId,
222
+ };
223
+ if (o.effort) {
224
+ payload.reasoning = { effort: o.effort };
225
+ if (o.summary && o.summary !== "none") payload.reasoning.summary = o.summary;
226
+ }
227
+ const tools = convertTools(anth.tools, o);
228
+ if (tools.length) {
229
+ payload.tools = tools;
230
+ payload.tool_choice = convertToolChoice(anth.tool_choice);
231
+ payload.parallel_tool_calls = o.parallelToolCalls && !anth.tool_choice?.disable_parallel_tool_use;
232
+ }
233
+ for (const p of o.removedParams || []) {
234
+ const [a, b] = p.split(".");
235
+ if (b && payload[a] && typeof payload[a] === "object") delete payload[a][b];
236
+ else delete payload[a];
237
+ }
238
+ return payload;
239
+ }