@stage-labs/metro 0.1.0-beta.70 → 0.1.0-beta.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bedrock.js +4 -73
- package/dist/claude-settings.js +40 -0
- package/dist/claude.js +101 -9
- package/dist/hold.js +20 -6
- package/dist/provider-flags.js +9 -0
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +9 -3
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +3 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/model-api.ts +169 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/bedrock.ts +252 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-auth.ts +231 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-device.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-stream.ts +288 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex-translate.ts +203 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/codex.ts +211 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/eventstream.ts +123 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/forward.ts +169 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/gateway.ts +193 -0
- package/runtime/node_modules/@metro-labs/mcp/src/gateway/model-config.ts +180 -0
- package/runtime/runtime.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { isRecord } from '../daemon/is-record.js';
|
|
3
|
+
|
|
4
|
+
export const SIGNATURE_PREFIX = 'metro-codex:';
|
|
5
|
+
const DEFAULT_EFFORT = 'medium';
|
|
6
|
+
const EFFORTS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh']);
|
|
7
|
+
const EFFORT_ALIAS: Record<string, string> = { max: 'xhigh' };
|
|
8
|
+
const IMAGE_NOTE = '[an image was attached here; this model cannot see it]';
|
|
9
|
+
const NAME_MAX = 64;
|
|
10
|
+
const NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
11
|
+
const HASH_LEN = 8;
|
|
12
|
+
|
|
13
|
+
type Item = Record<string, unknown>;
|
|
14
|
+
|
|
15
|
+
export interface TranslateOptions {
|
|
16
|
+
promptCacheKey: string;
|
|
17
|
+
names?: ToolNames;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ResponsesRequest {
|
|
21
|
+
model: string;
|
|
22
|
+
instructions?: string;
|
|
23
|
+
input: Item[];
|
|
24
|
+
tools: Item[];
|
|
25
|
+
tool_choice: unknown;
|
|
26
|
+
parallel_tool_calls: boolean;
|
|
27
|
+
reasoning: { effort: string; summary: string };
|
|
28
|
+
store: false;
|
|
29
|
+
stream: true;
|
|
30
|
+
include: string[];
|
|
31
|
+
prompt_cache_key: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const textOf = (value: unknown): string => (typeof value === 'string' ? value : '');
|
|
35
|
+
|
|
36
|
+
export function codexToolName(name: string): string {
|
|
37
|
+
if (name.length <= NAME_MAX && NAME_RE.test(name)) return name;
|
|
38
|
+
const clean = name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
39
|
+
const hash = createHash('sha256').update(name).digest('hex').slice(0, HASH_LEN);
|
|
40
|
+
return `${clean.slice(0, NAME_MAX - HASH_LEN - 1)}_${hash}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class ToolNames {
|
|
44
|
+
private readonly original = new Map<string, string>();
|
|
45
|
+
|
|
46
|
+
alias(name: string): string {
|
|
47
|
+
const alias = codexToolName(name);
|
|
48
|
+
if (alias !== name) this.original.set(alias, name);
|
|
49
|
+
return alias;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
restore(alias: string): string {
|
|
53
|
+
return this.original.get(alias) ?? alias;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function blocksText(content: unknown): string {
|
|
58
|
+
if (typeof content === 'string') return content;
|
|
59
|
+
if (!Array.isArray(content)) return '';
|
|
60
|
+
return content
|
|
61
|
+
.filter(isRecord)
|
|
62
|
+
.filter((block) => block.type === 'text')
|
|
63
|
+
.map((block) => textOf(block.text))
|
|
64
|
+
.join('\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function systemText(system: unknown): string {
|
|
68
|
+
return blocksText(system).trim();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function encodeSignature(item: Item): string {
|
|
72
|
+
return `${SIGNATURE_PREFIX}${Buffer.from(JSON.stringify(item)).toString('base64url')}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function decodeSignature(signature: unknown): Item | null {
|
|
76
|
+
if (typeof signature !== 'string' || !signature.startsWith(SIGNATURE_PREFIX)) return null;
|
|
77
|
+
try {
|
|
78
|
+
const parsed: unknown = JSON.parse(Buffer.from(signature.slice(SIGNATURE_PREFIX.length), 'base64url').toString('utf8'));
|
|
79
|
+
return isRecord(parsed) ? parsed : null;
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function userPart(block: Item): Item | null {
|
|
86
|
+
if (block.type === 'text') return { type: 'input_text', text: textOf(block.text) };
|
|
87
|
+
if (block.type === 'image' && isRecord(block.source) && block.source.type === 'base64')
|
|
88
|
+
return { type: 'input_image', image_url: `data:${textOf(block.source.media_type)};base64,${textOf(block.source.data)}` };
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const partText = (part: Item): string => (part.type === 'text' ? textOf(part.text) : part.type === 'image' ? IMAGE_NOTE : '');
|
|
93
|
+
|
|
94
|
+
function resultText(block: Item): string {
|
|
95
|
+
const content = block.content;
|
|
96
|
+
const text = typeof content === 'string' ? content : Array.isArray(content) ? content.filter(isRecord).map(partText).filter((t) => t !== '').join('\n') : '';
|
|
97
|
+
return block.is_error === true ? `[tool error] ${text}` : text;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function userItems(content: unknown): Item[] {
|
|
101
|
+
if (typeof content === 'string') return [{ type: 'message', role: 'user', content: [{ type: 'input_text', text: content }] }];
|
|
102
|
+
if (!Array.isArray(content)) return [];
|
|
103
|
+
const out: Item[] = [];
|
|
104
|
+
let parts: Item[] = [];
|
|
105
|
+
const flush = (): void => {
|
|
106
|
+
if (parts.length > 0) out.push({ type: 'message', role: 'user', content: parts });
|
|
107
|
+
parts = [];
|
|
108
|
+
};
|
|
109
|
+
for (const block of content.filter(isRecord)) {
|
|
110
|
+
if (block.type === 'tool_result') {
|
|
111
|
+
flush();
|
|
112
|
+
out.push({ type: 'function_call_output', call_id: textOf(block.tool_use_id), output: resultText(block) });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const part = userPart(block);
|
|
116
|
+
if (part !== null) parts.push(part);
|
|
117
|
+
}
|
|
118
|
+
flush();
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function assistantItems(content: unknown, names: ToolNames): Item[] {
|
|
123
|
+
if (typeof content === 'string') return [{ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: content }] }];
|
|
124
|
+
if (!Array.isArray(content)) return [];
|
|
125
|
+
const out: Item[] = [];
|
|
126
|
+
for (const block of content.filter(isRecord)) {
|
|
127
|
+
if (block.type === 'text') out.push({ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: textOf(block.text) }] });
|
|
128
|
+
else if (block.type === 'tool_use')
|
|
129
|
+
out.push({ type: 'function_call', call_id: textOf(block.id), name: names.alias(textOf(block.name)), arguments: JSON.stringify(block.input ?? {}) });
|
|
130
|
+
else if (block.type === 'thinking') {
|
|
131
|
+
const reasoning = decodeSignature(block.signature);
|
|
132
|
+
if (reasoning !== null) out.push({ type: 'reasoning', ...reasoning, summary: [] });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function inputItems(messages: unknown, names = new ToolNames()): Item[] {
|
|
139
|
+
const out: Item[] = [];
|
|
140
|
+
if (!Array.isArray(messages)) return out;
|
|
141
|
+
for (const message of messages.filter(isRecord)) {
|
|
142
|
+
if (message.role === 'assistant') out.push(...assistantItems(message.content, names));
|
|
143
|
+
else out.push(...userItems(message.content));
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function toolItems(tools: unknown, names = new ToolNames()): Item[] {
|
|
149
|
+
if (!Array.isArray(tools)) return [];
|
|
150
|
+
return tools
|
|
151
|
+
.filter(isRecord)
|
|
152
|
+
.filter((tool) => typeof tool.name === 'string' && isRecord(tool.input_schema))
|
|
153
|
+
.map((tool) => ({
|
|
154
|
+
type: 'function',
|
|
155
|
+
name: names.alias(textOf(tool.name)),
|
|
156
|
+
description: textOf(tool.description),
|
|
157
|
+
parameters: tool.input_schema,
|
|
158
|
+
strict: false,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function toolChoiceOf(choice: unknown, names = new ToolNames()): unknown {
|
|
163
|
+
if (!isRecord(choice)) return 'auto';
|
|
164
|
+
if (choice.type === 'any') return 'required';
|
|
165
|
+
if (choice.type === 'none') return 'none';
|
|
166
|
+
if (choice.type === 'tool' && typeof choice.name === 'string') return { type: 'function', name: names.alias(choice.name) };
|
|
167
|
+
return 'auto';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function configuredEffort(body: Item): string | null {
|
|
171
|
+
const config = isRecord(body.output_config) ? body.output_config : {};
|
|
172
|
+
const effort = typeof config.effort === 'string' ? (EFFORT_ALIAS[config.effort] ?? config.effort) : '';
|
|
173
|
+
return EFFORTS.has(effort) ? effort : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function effortOf(body: Item): string {
|
|
177
|
+
const configured = configuredEffort(body);
|
|
178
|
+
if (configured !== null) return configured;
|
|
179
|
+
const thinking = isRecord(body.thinking) ? body.thinking : {};
|
|
180
|
+
if (thinking.type === 'disabled') return 'low';
|
|
181
|
+
const budget = typeof thinking.budget_tokens === 'number' ? thinking.budget_tokens : 0;
|
|
182
|
+
if (budget > 0 && budget <= 4_000) return 'low';
|
|
183
|
+
if (budget > 16_000) return 'high';
|
|
184
|
+
return DEFAULT_EFFORT;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function toResponsesRequest(body: Item, model: string, opts: TranslateOptions): ResponsesRequest {
|
|
188
|
+
const system = systemText(body.system);
|
|
189
|
+
const names = opts.names ?? new ToolNames();
|
|
190
|
+
return {
|
|
191
|
+
model,
|
|
192
|
+
...(system === '' ? {} : { instructions: system }),
|
|
193
|
+
input: inputItems(body.messages, names),
|
|
194
|
+
tools: toolItems(body.tools, names),
|
|
195
|
+
tool_choice: toolChoiceOf(body.tool_choice, names),
|
|
196
|
+
parallel_tool_calls: true,
|
|
197
|
+
reasoning: { effort: effortOf(body), summary: 'auto' },
|
|
198
|
+
store: false,
|
|
199
|
+
stream: true,
|
|
200
|
+
include: ['reasoning.encrypted_content'],
|
|
201
|
+
prompt_cache_key: opts.promptCacheKey,
|
|
202
|
+
};
|
|
203
|
+
}
|