@stage-labs/metro 0.1.0-beta.70 → 0.1.0-beta.72

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 (25) hide show
  1. package/dist/bedrock.js +4 -73
  2. package/dist/claude-settings.js +40 -0
  3. package/dist/claude.js +101 -9
  4. package/dist/hold.js +20 -6
  5. package/dist/provider-flags.js +9 -0
  6. package/package.json +1 -1
  7. package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-api.ts +22 -4
  8. package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-settings.ts +117 -0
  9. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +9 -3
  10. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +3 -0
  11. package/runtime/node_modules/@metro-labs/mcp/src/daemon/model-api.ts +192 -0
  12. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +5 -0
  13. package/runtime/node_modules/@metro-labs/mcp/src/daemon/terminal-ws.ts +18 -9
  14. package/runtime/node_modules/@metro-labs/mcp/src/gateway/bedrock.ts +252 -0
  15. package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-auth.ts +231 -0
  16. package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-device.ts +91 -0
  17. package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-stream.ts +288 -0
  18. package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-translate.ts +203 -0
  19. package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex.ts +211 -0
  20. package/runtime/node_modules/@metro-labs/mcp/src/gateway/eventstream.ts +123 -0
  21. package/runtime/node_modules/@metro-labs/mcp/src/gateway/forward.ts +169 -0
  22. package/runtime/node_modules/@metro-labs/mcp/src/gateway/gateway.ts +193 -0
  23. package/runtime/node_modules/@metro-labs/mcp/src/gateway/model-config.ts +180 -0
  24. package/runtime/node_modules/@metro-labs/mcp/src/gateway/openrouter.ts +31 -0
  25. package/runtime/runtime.json +1 -1
@@ -0,0 +1,231 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { isRecord } from '../daemon/is-record.js';
6
+
7
+ export const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
8
+ export const CODEX_ISSUER = 'https://auth.openai.com';
9
+ export const CODEX_REDIRECT = 'http://localhost:1455/auth/callback';
10
+ const SCOPE = 'openid profile email offline_access api.connectors.read api.connectors.invoke';
11
+ const AUTH_CLAIM = 'https://api.openai.com/auth';
12
+ const PENDING_TTL_MS = 10 * 60_000;
13
+ export const ACCESS_TOKEN_TTL_MS = 55 * 60_000;
14
+
15
+ export interface CodexTokens {
16
+ accessToken: string;
17
+ refreshToken: string;
18
+ idToken: string;
19
+ accountId: string;
20
+ email: string | null;
21
+ plan: string | null;
22
+ savedAt: string;
23
+ }
24
+
25
+ export class CodexAuthError extends Error {}
26
+
27
+ interface Pending {
28
+ verifier: string;
29
+ at: number;
30
+ }
31
+
32
+ const pending = new Map<string, Pending>();
33
+
34
+ const b64url = (buf: Buffer): string => buf.toString('base64url');
35
+
36
+ export function newPkce(): { verifier: string; challenge: string } {
37
+ const verifier = b64url(randomBytes(32));
38
+ const challenge = b64url(createHash('sha256').update(verifier).digest());
39
+ return { verifier, challenge };
40
+ }
41
+
42
+ export function authorizeUrl(state: string, challenge: string, issuer = CODEX_ISSUER): string {
43
+ const query = new URLSearchParams({
44
+ response_type: 'code',
45
+ client_id: CODEX_CLIENT_ID,
46
+ redirect_uri: CODEX_REDIRECT,
47
+ scope: SCOPE,
48
+ code_challenge: challenge,
49
+ code_challenge_method: 'S256',
50
+ id_token_add_organizations: 'true',
51
+ codex_cli_simplified_flow: 'true',
52
+ state,
53
+ originator: 'codex_cli_rs',
54
+ });
55
+ return `${issuer}/oauth/authorize?${query.toString()}`;
56
+ }
57
+
58
+ function sweep(now: number): void {
59
+ for (const [state, entry] of pending) if (now - entry.at > PENDING_TTL_MS) pending.delete(state);
60
+ }
61
+
62
+ export function beginLogin(issuer = CODEX_ISSUER, now = Date.now()): { url: string; state: string } {
63
+ sweep(now);
64
+ const { verifier, challenge } = newPkce();
65
+ const state = b64url(randomBytes(24));
66
+ pending.set(state, { verifier, at: now });
67
+ return { url: authorizeUrl(state, challenge, issuer), state };
68
+ }
69
+
70
+ export function parseCallback(raw: string): { code: string; state: string } {
71
+ const text = raw.trim();
72
+ let query: URLSearchParams;
73
+ try {
74
+ query = new URL(text).searchParams;
75
+ } catch {
76
+ query = new URLSearchParams(text.replace(/^\?/, ''));
77
+ }
78
+ const code = query.get('code')?.trim() ?? '';
79
+ const state = query.get('state')?.trim() ?? '';
80
+ if (code === '' || state === '')
81
+ throw new CodexAuthError('paste the whole address the browser landed on; it carries code= and state=');
82
+ return { code, state };
83
+ }
84
+
85
+ function decodeJwtPayload(jwt: string): Record<string, unknown> {
86
+ const part = jwt.split('.')[1] ?? '';
87
+ try {
88
+ const parsed: unknown = JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
89
+ return isRecord(parsed) ? parsed : {};
90
+ } catch {
91
+ return {};
92
+ }
93
+ }
94
+
95
+ const text = (value: unknown): string | null => (typeof value === 'string' && value !== '' ? value : null);
96
+
97
+ export function claimsOf(idToken: string): { accountId: string | null; email: string | null; plan: string | null } {
98
+ const payload = decodeJwtPayload(idToken);
99
+ const auth = isRecord(payload[AUTH_CLAIM]) ? payload[AUTH_CLAIM] : {};
100
+ return { accountId: text(auth.chatgpt_account_id), email: text(payload.email), plan: text(auth.chatgpt_plan_type) };
101
+ }
102
+
103
+ interface TokenResponse {
104
+ id_token?: unknown;
105
+ access_token?: unknown;
106
+ refresh_token?: unknown;
107
+ }
108
+
109
+ const EMPTY_TOKENS: CodexTokens = { accessToken: '', refreshToken: '', idToken: '', accountId: '', email: null, plan: null, savedAt: '' };
110
+
111
+ function assertUsable(next: CodexTokens): CodexTokens {
112
+ if (next.accessToken === '' || next.refreshToken === '') throw new CodexAuthError('the token endpoint returned no usable tokens');
113
+ if (next.accountId === '') throw new CodexAuthError('the id token carries no ChatGPT account id; sign in with a ChatGPT account that has Codex');
114
+ return next;
115
+ }
116
+
117
+ const pick = (fresh: string | null, kept: string | null): string | null => fresh ?? kept;
118
+
119
+ export function tokensFrom(body: unknown, previous: CodexTokens | null, now = new Date()): CodexTokens {
120
+ const res = (isRecord(body) ? body : {}) as TokenResponse;
121
+ const kept = previous ?? EMPTY_TOKENS;
122
+ const idToken = pick(text(res.id_token), kept.idToken) ?? '';
123
+ const claims = claimsOf(idToken);
124
+ return assertUsable({
125
+ accessToken: text(res.access_token) ?? '',
126
+ refreshToken: pick(text(res.refresh_token), kept.refreshToken) ?? '',
127
+ idToken,
128
+ accountId: pick(claims.accountId, kept.accountId) ?? '',
129
+ email: pick(claims.email, kept.email),
130
+ plan: pick(claims.plan, kept.plan),
131
+ savedAt: now.toISOString(),
132
+ });
133
+ }
134
+
135
+ async function tokenCall(issuer: string, init: RequestInit, fetchImpl: typeof fetch): Promise<unknown> {
136
+ const res = await fetchImpl(`${issuer}/oauth/token`, init);
137
+ const raw = await res.text();
138
+ if (!res.ok) {
139
+ let detail = raw.slice(0, 300);
140
+ try {
141
+ const parsed: unknown = JSON.parse(raw);
142
+ if (isRecord(parsed)) detail = text(parsed.error_description) ?? text(parsed.error) ?? detail;
143
+ } catch {
144
+ detail = raw.slice(0, 300);
145
+ }
146
+ throw new CodexAuthError(`OpenAI refused the token request (${String(res.status)}): ${detail}`);
147
+ }
148
+ return JSON.parse(raw) as unknown;
149
+ }
150
+
151
+ export async function finishLogin(
152
+ callback: string,
153
+ issuer = CODEX_ISSUER,
154
+ fetchImpl: typeof fetch = fetch,
155
+ now = Date.now(),
156
+ ): Promise<CodexTokens> {
157
+ sweep(now);
158
+ const { code, state } = parseCallback(callback);
159
+ const entry = pending.get(state);
160
+ if (entry === undefined) throw new CodexAuthError('this sign-in link has expired or was started elsewhere; press Connect again');
161
+ pending.delete(state);
162
+ return exchangeCode(issuer, code, entry.verifier, CODEX_REDIRECT, fetchImpl, now);
163
+ }
164
+
165
+ export async function exchangeCode(
166
+ issuer: string,
167
+ code: string,
168
+ verifier: string,
169
+ redirectUri: string,
170
+ fetchImpl: typeof fetch,
171
+ now = Date.now(),
172
+ ): Promise<CodexTokens> {
173
+ const body = new URLSearchParams({
174
+ grant_type: 'authorization_code',
175
+ code,
176
+ redirect_uri: redirectUri,
177
+ client_id: CODEX_CLIENT_ID,
178
+ code_verifier: verifier,
179
+ });
180
+ const res = await tokenCall(
181
+ issuer,
182
+ { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: body.toString() },
183
+ fetchImpl,
184
+ );
185
+ return tokensFrom(res, null, new Date(now));
186
+ }
187
+
188
+ export async function refreshTokens(previous: CodexTokens, issuer = CODEX_ISSUER, fetchImpl: typeof fetch = fetch): Promise<CodexTokens> {
189
+ const res = await tokenCall(
190
+ issuer,
191
+ {
192
+ method: 'POST',
193
+ headers: { 'content-type': 'application/json' },
194
+ body: JSON.stringify({ client_id: CODEX_CLIENT_ID, grant_type: 'refresh_token', refresh_token: previous.refreshToken }),
195
+ },
196
+ fetchImpl,
197
+ );
198
+ return tokensFrom(res, previous);
199
+ }
200
+
201
+ export const tokensStale = (tokens: CodexTokens, now = Date.now()): boolean =>
202
+ now - Date.parse(tokens.savedAt) > ACCESS_TOKEN_TTL_MS;
203
+
204
+ function readAuthFile(home: string): Record<string, unknown> {
205
+ let raw: unknown;
206
+ try {
207
+ raw = JSON.parse(readFileSync(join(home, '.codex', 'auth.json'), 'utf8'));
208
+ } catch {
209
+ throw new CodexAuthError('no Codex CLI login on this machine (~/.codex/auth.json); run codex login there first, or connect here');
210
+ }
211
+ return isRecord(raw) ? raw : {};
212
+ }
213
+
214
+ export function readCodexCliAuth(home = homedir()): CodexTokens {
215
+ const raw = readAuthFile(home);
216
+ if (!isRecord(raw.tokens)) throw new CodexAuthError('~/.codex/auth.json holds no ChatGPT tokens (an API-key login cannot be reused here)');
217
+ const stored = raw.tokens;
218
+ const idToken = text(stored.id_token) ?? '';
219
+ const claims = claimsOf(idToken);
220
+ const found: CodexTokens = {
221
+ accessToken: text(stored.access_token) ?? '',
222
+ refreshToken: text(stored.refresh_token) ?? '',
223
+ idToken,
224
+ accountId: text(stored.account_id) ?? claims.accountId ?? '',
225
+ email: claims.email,
226
+ plan: claims.plan,
227
+ savedAt: text(raw.last_refresh) ?? new Date(0).toISOString(),
228
+ };
229
+ if ([found.accessToken, found.refreshToken, found.accountId].includes('')) throw new CodexAuthError('~/.codex/auth.json is missing a token or the account id');
230
+ return found;
231
+ }
@@ -0,0 +1,91 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { isRecord } from '../daemon/is-record.js';
3
+ import { errMsg } from '../daemon/log.js';
4
+ import { CODEX_CLIENT_ID, CODEX_ISSUER, CodexAuthError, exchangeCode, type CodexTokens } from './codex-auth.js';
5
+
6
+ const VERIFY_PATH = '/codex/device';
7
+ const DEVICE_TTL_MS = 15 * 60_000;
8
+ const DEFAULT_INTERVAL_S = 5;
9
+ const ID_BYTES = 18;
10
+
11
+ export interface DeviceLogin {
12
+ id: string;
13
+ userCode: string;
14
+ verifyUrl: string;
15
+ interval: number;
16
+ }
17
+
18
+ export type DeviceStatus = { status: 'pending' } | { status: 'done'; tokens: CodexTokens } | { status: 'failed'; error: string };
19
+
20
+ interface Entry {
21
+ deviceAuthId: string;
22
+ userCode: string;
23
+ interval: number;
24
+ issuer: string;
25
+ startedAt: number;
26
+ polledAt: number | null;
27
+ }
28
+
29
+ const logins = new Map<string, Entry>();
30
+
31
+ const str = (value: unknown): string => (typeof value === 'string' ? value : '');
32
+
33
+ function intervalOf(value: unknown): number {
34
+ const n = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : NaN;
35
+ return Number.isFinite(n) && n >= 1 ? Math.round(n) : DEFAULT_INTERVAL_S;
36
+ }
37
+
38
+ function sweep(now: number): void {
39
+ for (const [id, entry] of logins) if (now - entry.startedAt > DEVICE_TTL_MS) logins.delete(id);
40
+ }
41
+
42
+ const post = (url: string, body: unknown, fetchImpl: typeof fetch): Promise<Response> =>
43
+ fetchImpl(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
44
+
45
+ export async function beginDeviceLogin(issuer = CODEX_ISSUER, fetchImpl: typeof fetch = fetch, now = Date.now()): Promise<DeviceLogin> {
46
+ sweep(now);
47
+ const res = await post(`${issuer}/api/accounts/deviceauth/usercode`, { client_id: CODEX_CLIENT_ID }, fetchImpl);
48
+ if (res.status === 404) throw new CodexAuthError('OpenAI is not offering the device-code sign-in right now; use the browser sign-in instead');
49
+ if (!res.ok) throw new CodexAuthError(`OpenAI refused to start the device-code sign-in (${String(res.status)})`);
50
+ const body: unknown = await res.json();
51
+ const parsed = isRecord(body) ? body : {};
52
+ const deviceAuthId = str(parsed.device_auth_id);
53
+ const userCode = str(parsed.user_code) || str(parsed.usercode);
54
+ if (deviceAuthId === '' || userCode === '') throw new CodexAuthError('OpenAI answered without a device code');
55
+ const id = randomBytes(ID_BYTES).toString('base64url');
56
+ const interval = intervalOf(parsed.interval);
57
+ logins.set(id, { deviceAuthId, userCode, interval, issuer, startedAt: now, polledAt: null });
58
+ return { id, userCode, verifyUrl: `${issuer}${VERIFY_PATH}`, interval };
59
+ }
60
+
61
+ async function claim(entry: Entry, fetchImpl: typeof fetch, now: number): Promise<DeviceStatus> {
62
+ const res = await post(`${entry.issuer}/api/accounts/deviceauth/token`, { device_auth_id: entry.deviceAuthId, user_code: entry.userCode }, fetchImpl);
63
+ if (res.status === 403 || res.status === 404) {
64
+ await res.body?.cancel();
65
+ return { status: 'pending' };
66
+ }
67
+ if (!res.ok) return { status: 'failed', error: `OpenAI ended the device-code sign-in (${String(res.status)})` };
68
+ const body: unknown = await res.json();
69
+ const parsed = isRecord(body) ? body : {};
70
+ const code = str(parsed.authorization_code);
71
+ const verifier = str(parsed.code_verifier);
72
+ if (code === '' || verifier === '') return { status: 'failed', error: 'OpenAI approved the sign-in but sent no code back' };
73
+ const tokens = await exchangeCode(entry.issuer, code, verifier, `${entry.issuer}/deviceauth/callback`, fetchImpl, now);
74
+ return { status: 'done', tokens };
75
+ }
76
+
77
+ export async function pollDeviceLogin(id: string, fetchImpl: typeof fetch = fetch, now = Date.now()): Promise<DeviceStatus> {
78
+ sweep(now);
79
+ const entry = logins.get(id);
80
+ if (entry === undefined) throw new CodexAuthError('this sign-in has expired or already finished; press Connect again');
81
+ if (entry.polledAt !== null && now - entry.polledAt < entry.interval * 1000) return { status: 'pending' };
82
+ entry.polledAt = now;
83
+ let result: DeviceStatus;
84
+ try {
85
+ result = await claim(entry, fetchImpl, now);
86
+ } catch (err) {
87
+ result = { status: 'failed', error: errMsg(err) };
88
+ }
89
+ if (result.status !== 'pending') logins.delete(id);
90
+ return result;
91
+ }
@@ -0,0 +1,288 @@
1
+ import { isRecord } from '../daemon/is-record.js';
2
+ import { encodeSignature } from './codex-translate.js';
3
+
4
+ type Item = Record<string, unknown>;
5
+
6
+ interface Block {
7
+ index: number;
8
+ kind: 'text' | 'thinking' | 'tool_use';
9
+ deltas: number;
10
+ }
11
+
12
+ interface Usage {
13
+ input_tokens: number;
14
+ output_tokens: number;
15
+ cache_read_input_tokens: number;
16
+ }
17
+
18
+ const frame = (event: string, data: unknown): string => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
19
+ const str = (value: unknown): string => (typeof value === 'string' ? value : '');
20
+ const responseOf = (data: Item): Item => (isRecord(data.response) ? data.response : {});
21
+ const itemOf = (data: Item): Item => (isRecord(data.item) ? data.item : {});
22
+
23
+ function errorKind(code: string): string {
24
+ if (code === 'rate_limit_exceeded' || code === 'usage_limit_reached') return 'rate_limit_error';
25
+ if (code === 'context_length_exceeded' || code === 'invalid_prompt') return 'invalid_request_error';
26
+ if (code === 'insufficient_quota') return 'permission_error';
27
+ return 'api_error';
28
+ }
29
+
30
+ function usageOf(response: Item): Usage {
31
+ const usage = isRecord(response.usage) ? response.usage : {};
32
+ const details = isRecord(usage.input_tokens_details) ? usage.input_tokens_details : {};
33
+ const cached = typeof details.cached_tokens === 'number' ? details.cached_tokens : 0;
34
+ const input = typeof usage.input_tokens === 'number' ? usage.input_tokens : 0;
35
+ return {
36
+ input_tokens: Math.max(0, input - cached),
37
+ output_tokens: typeof usage.output_tokens === 'number' ? usage.output_tokens : 0,
38
+ cache_read_input_tokens: cached,
39
+ };
40
+ }
41
+
42
+ const startBlock = (kind: Block['kind'], id: string, name: string): Item =>
43
+ kind === 'text' ? { type: 'text', text: '' } : kind === 'thinking' ? { type: 'thinking', thinking: '' } : { type: 'tool_use', id, name, input: {} };
44
+
45
+ const messageText = (item: Item): string =>
46
+ Array.isArray(item.content) ? item.content.filter(isRecord).map((part) => str(part.text)).join('') : '';
47
+
48
+ export class CodexEventTranslator {
49
+ private readonly blocks = new Map<string, Block>();
50
+ private next = 0;
51
+ private started = false;
52
+ private toolCalls = 0;
53
+ private done = false;
54
+ private readonly model: string;
55
+ private readonly restore: (name: string) => string;
56
+ private readonly handlers: Record<string, (data: Item) => string> = {
57
+ 'response.created': (d) => this.start(responseOf(d)),
58
+ 'response.output_item.added': (d) => this.start({}) + this.itemAdded(itemOf(d)),
59
+ 'response.output_text.delta': (d) => this.delta(str(d.item_id), 'text', str(d.delta)),
60
+ 'response.reasoning_summary_text.delta': (d) => this.delta(str(d.item_id), 'thinking', str(d.delta)),
61
+ 'response.reasoning_summary_part.added': (d) => this.partAdded(str(d.item_id)),
62
+ 'response.function_call_arguments.delta': (d) => this.delta(str(d.item_id), 'tool_use', str(d.delta)),
63
+ 'response.output_item.done': (d) => this.itemDone(itemOf(d)),
64
+ 'response.completed': (d) => this.start(responseOf(d)) + this.completed(responseOf(d)),
65
+ 'response.failed': (d) => this.failed(responseOf(d), 'Codex failed the response'),
66
+ 'response.incomplete': (d) => this.incomplete(responseOf(d)),
67
+ error: (d) => this.failed({ error: d }, 'Codex stream error'),
68
+ };
69
+
70
+ constructor(model: string, restore: (name: string) => string = (name) => name) {
71
+ this.model = model;
72
+ this.restore = restore;
73
+ }
74
+
75
+ get finished(): boolean {
76
+ return this.done;
77
+ }
78
+
79
+ private open(key: string, kind: Block['kind'], id = '', name = ''): string {
80
+ const block: Block = { index: this.next, kind, deltas: 0 };
81
+ this.next += 1;
82
+ this.blocks.set(key, block);
83
+ return frame('content_block_start', { type: 'content_block_start', index: block.index, content_block: startBlock(kind, id, name) });
84
+ }
85
+
86
+ private start(response: Item): string {
87
+ if (this.started) return '';
88
+ this.started = true;
89
+ return frame('message_start', {
90
+ type: 'message_start',
91
+ message: {
92
+ id: str(response.id) || 'msg_codex',
93
+ type: 'message',
94
+ role: 'assistant',
95
+ model: this.model,
96
+ content: [],
97
+ stop_reason: null,
98
+ stop_sequence: null,
99
+ usage: { input_tokens: 0, output_tokens: 0 },
100
+ },
101
+ });
102
+ }
103
+
104
+ private openCall(key: string, item: Item): string {
105
+ this.toolCalls += 1;
106
+ return this.open(key, 'tool_use', str(item.call_id) || key, this.restore(str(item.name)));
107
+ }
108
+
109
+ private itemAdded(item: Item): string {
110
+ const key = str(item.id) || String(this.next);
111
+ if (item.type === 'function_call') return this.openCall(key, item);
112
+ if (item.type === 'reasoning') return this.open(key, 'thinking');
113
+ return '';
114
+ }
115
+
116
+ private partAdded(key: string): string {
117
+ const block = this.blocks.get(key);
118
+ return block !== undefined && block.deltas > 0 ? this.delta(key, 'thinking', '\n\n') : '';
119
+ }
120
+
121
+ private delta(key: string, kind: Block['kind'], text: string): string {
122
+ const opened = this.blocks.has(key) ? '' : this.open(key, kind);
123
+ const block = this.blocks.get(key);
124
+ if (block === undefined) return opened;
125
+ block.deltas += 1;
126
+ const delta =
127
+ kind === 'tool_use' ? { type: 'input_json_delta', partial_json: text } : kind === 'text' ? { type: 'text_delta', text } : { type: 'thinking_delta', thinking: text };
128
+ return opened + frame('content_block_delta', { type: 'content_block_delta', index: block.index, delta });
129
+ }
130
+
131
+ private stop(key: string): string {
132
+ const block = this.blocks.get(key);
133
+ if (block === undefined) return '';
134
+ this.blocks.delete(key);
135
+ return frame('content_block_stop', { type: 'content_block_stop', index: block.index });
136
+ }
137
+
138
+ private reasoningDone(key: string, item: Item): string {
139
+ const opened = this.blocks.has(key) ? '' : this.open(key, 'thinking');
140
+ const block = this.blocks.get(key);
141
+ if (block === undefined || typeof item.encrypted_content !== 'string') return opened + this.stop(key);
142
+ const signature = encodeSignature({ id: key, encrypted_content: item.encrypted_content });
143
+ return opened + frame('content_block_delta', { type: 'content_block_delta', index: block.index, delta: { type: 'signature_delta', signature } }) + this.stop(key);
144
+ }
145
+
146
+ private callDone(key: string, item: Item): string {
147
+ let out = this.blocks.has(key) ? '' : this.openCall(key, item);
148
+ if (this.blocks.get(key)?.deltas === 0 && str(item.arguments) !== '') out += this.delta(key, 'tool_use', str(item.arguments));
149
+ return out + this.stop(key);
150
+ }
151
+
152
+ private itemDone(item: Item): string {
153
+ const key = str(item.id);
154
+ if (item.type === 'reasoning') return this.reasoningDone(key, item);
155
+ if (item.type === 'function_call') return this.callDone(key, item);
156
+ if (item.type === 'message' && !this.blocks.has(key) && messageText(item) !== '') return this.delta(key, 'text', messageText(item)) + this.stop(key);
157
+ return this.stop(key);
158
+ }
159
+
160
+ private completed(response: Item, forced: string | null = null): string {
161
+ this.done = true;
162
+ let out = '';
163
+ for (const key of [...this.blocks.keys()]) out += this.stop(key);
164
+ const stop = forced ?? (this.toolCalls > 0 ? 'tool_use' : 'end_turn');
165
+ out += frame('message_delta', { type: 'message_delta', delta: { stop_reason: stop, stop_sequence: null }, usage: usageOf(response) });
166
+ return out + frame('message_stop', { type: 'message_stop' });
167
+ }
168
+
169
+ private incomplete(response: Item): string {
170
+ const details = isRecord(response.incomplete_details) ? response.incomplete_details : {};
171
+ if (details.reason === 'max_output_tokens') return this.start(response) + this.completed(response, 'max_tokens');
172
+ return this.failed(response, `Codex left the response incomplete (${str(details.reason) || 'no reason given'})`);
173
+ }
174
+
175
+ private failed(response: Item, fallback: string): string {
176
+ this.done = true;
177
+ const error = isRecord(response.error) ? response.error : {};
178
+ return frame('error', { type: 'error', error: { type: errorKind(str(error.code)), message: str(error.message) || fallback } });
179
+ }
180
+
181
+ push(event: string, data: Item): string {
182
+ const handler = this.handlers[event];
183
+ return handler === undefined ? '' : handler(data);
184
+ }
185
+
186
+ close(message = 'Codex ended the stream before completing the response'): string {
187
+ if (this.done) return '';
188
+ this.done = true;
189
+ return frame('error', { type: 'error', error: { type: 'api_error', message } });
190
+ }
191
+ }
192
+
193
+ export interface SseEvent {
194
+ event: string;
195
+ data: string;
196
+ }
197
+
198
+ export class SseParser {
199
+ private pending = '';
200
+
201
+ push(chunk: string): SseEvent[] {
202
+ this.pending += chunk;
203
+ const out: SseEvent[] = [];
204
+ for (;;) {
205
+ const at = this.pending.indexOf('\n\n');
206
+ if (at < 0) break;
207
+ const raw = this.pending.slice(0, at);
208
+ this.pending = this.pending.slice(at + 2);
209
+ let event = '';
210
+ const data: string[] = [];
211
+ for (const line of raw.split('\n')) {
212
+ if (line.startsWith('event:')) event = line.slice(6).trim();
213
+ else if (line.startsWith('data:')) data.push(line.slice(5).trimStart());
214
+ }
215
+ if (data.length > 0) out.push({ event, data: data.join('\n') });
216
+ }
217
+ return out;
218
+ }
219
+ }
220
+
221
+ export function parseEvent(raw: SseEvent): { event: string; data: Item } | null {
222
+ let parsed: unknown;
223
+ try {
224
+ parsed = JSON.parse(raw.data);
225
+ } catch {
226
+ return null;
227
+ }
228
+ if (!isRecord(parsed)) return null;
229
+ const event = raw.event !== '' ? raw.event : str(parsed.type);
230
+ return event === '' ? null : { event, data: parsed };
231
+ }
232
+
233
+ interface Assembled {
234
+ message: Item;
235
+ blocks: Item[];
236
+ }
237
+
238
+ const FOLDS: Record<string, (state: Assembled, data: Item) => void> = {
239
+ message_start: (state, data) => {
240
+ if (isRecord(data.message)) state.message = { ...data.message };
241
+ },
242
+ content_block_start: (state, data) => {
243
+ if (isRecord(data.content_block)) state.blocks.push({ ...data.content_block });
244
+ },
245
+ content_block_delta: (state, data) => {
246
+ if (isRecord(data.delta)) applyDelta(state.blocks[typeof data.index === 'number' ? data.index : -1], data.delta);
247
+ },
248
+ message_delta: (state, data) => {
249
+ if (isRecord(data.delta)) state.message.stop_reason = data.delta.stop_reason ?? null;
250
+ if (isRecord(data.usage)) state.message.usage = data.usage;
251
+ },
252
+ error: (state, data) => {
253
+ state.message = { type: 'error', error: data.error };
254
+ },
255
+ };
256
+
257
+ function foldFrame(state: Assembled, event: string, data: Item): void {
258
+ FOLDS[event]?.(state, data);
259
+ }
260
+
261
+ function applyDelta(block: Item | undefined, delta: Item): void {
262
+ if (block === undefined) return;
263
+ if (delta.type === 'text_delta') block.text = str(block.text) + str(delta.text);
264
+ else if (delta.type === 'thinking_delta') block.thinking = str(block.thinking) + str(delta.thinking);
265
+ else if (delta.type === 'signature_delta') block.signature = str(delta.signature);
266
+ else if (delta.type === 'input_json_delta') block.json = str(block.json) + str(delta.partial_json);
267
+ }
268
+
269
+ export function assembleMessage(frames: string): Item {
270
+ const state: Assembled = { message: {}, blocks: [] };
271
+ for (const raw of new SseParser().push(frames)) {
272
+ const parsed = parseEvent(raw);
273
+ if (parsed !== null) foldFrame(state, parsed.event, parsed.data);
274
+ }
275
+ if (state.message.type === 'error') return state.message;
276
+ const content = state.blocks.map((block) => {
277
+ if (block.type !== 'tool_use') return block;
278
+ const { json, ...rest } = block;
279
+ let input: unknown = {};
280
+ try {
281
+ input = JSON.parse(str(json) || '{}');
282
+ } catch {
283
+ input = {};
284
+ }
285
+ return { ...rest, input };
286
+ });
287
+ return { ...state.message, content };
288
+ }