@bitkyc08/opencodex 2.7.34 → 2.7.35

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.
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Chat Completions inbound: OpenAI-compatible request -> internal /v1/responses body.
3
+ *
4
+ * Used by GitHub Copilot App (and other OpenAI-compatible clients) via POST /v1/chat/completions.
5
+ * Same translate-and-replay pattern as Claude Messages: the produced body must pass
6
+ * responsesRequestSchema so routing/OAuth/pool/sidecars are inherited unchanged.
7
+ */
8
+ export class ChatCompletionsRequestError extends Error {}
9
+
10
+ type Rec = Record<string, unknown>;
11
+
12
+ function isRec(v: unknown): v is Rec {
13
+ return !!v && typeof v === "object" && !Array.isArray(v);
14
+ }
15
+
16
+ const OUTPUT_CONFIG_EFFORTS = new Set(["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
17
+
18
+ function contentToText(content: unknown): string {
19
+ if (typeof content === "string") return content;
20
+ if (!Array.isArray(content)) return "";
21
+ const parts: string[] = [];
22
+ for (const raw of content) {
23
+ if (typeof raw === "string") {
24
+ parts.push(raw);
25
+ continue;
26
+ }
27
+ if (!isRec(raw)) continue;
28
+ if ((raw.type === "text" || raw.type === "input_text" || raw.type === "output_text") && typeof raw.text === "string") {
29
+ parts.push(raw.text);
30
+ }
31
+ }
32
+ return parts.join("\n");
33
+ }
34
+
35
+ function imageUrlFromPart(part: Rec): string | null {
36
+ if (part.type !== "image_url") return null;
37
+ const imageUrl = part.image_url;
38
+ if (typeof imageUrl === "string" && imageUrl.length > 0) return imageUrl;
39
+ if (isRec(imageUrl) && typeof imageUrl.url === "string" && imageUrl.url.length > 0) return imageUrl.url;
40
+ return null;
41
+ }
42
+
43
+ function userContentToBlocks(content: unknown): Rec[] {
44
+ if (typeof content === "string") {
45
+ return content.length > 0 ? [{ type: "input_text", text: content }] : [];
46
+ }
47
+ if (!Array.isArray(content)) return [];
48
+ const blocks: Rec[] = [];
49
+ for (const raw of content) {
50
+ if (typeof raw === "string") {
51
+ if (raw.length > 0) blocks.push({ type: "input_text", text: raw });
52
+ continue;
53
+ }
54
+ if (!isRec(raw)) continue;
55
+ if ((raw.type === "text" || raw.type === "input_text") && typeof raw.text === "string") {
56
+ blocks.push({ type: "input_text", text: raw.text });
57
+ continue;
58
+ }
59
+ const imageUrl = imageUrlFromPart(raw);
60
+ if (imageUrl) blocks.push({ type: "input_image", image_url: imageUrl });
61
+ }
62
+ return blocks;
63
+ }
64
+
65
+ function assistantContentToBlocks(content: unknown): Rec[] {
66
+ if (typeof content === "string") {
67
+ return content.length > 0 ? [{ type: "output_text", text: content }] : [];
68
+ }
69
+ if (!Array.isArray(content)) return [];
70
+ const blocks: Rec[] = [];
71
+ for (const raw of content) {
72
+ if (typeof raw === "string") {
73
+ if (raw.length > 0) blocks.push({ type: "output_text", text: raw });
74
+ continue;
75
+ }
76
+ if (!isRec(raw)) continue;
77
+ if ((raw.type === "text" || raw.type === "output_text") && typeof raw.text === "string") {
78
+ blocks.push({ type: "output_text", text: raw.text });
79
+ }
80
+ }
81
+ return blocks;
82
+ }
83
+
84
+ function pushSystemText(parts: string[], content: unknown): void {
85
+ const text = contentToText(content).trim();
86
+ if (text) parts.push(text);
87
+ }
88
+
89
+ function toolCallsToItems(toolCalls: unknown, input: Rec[]): void {
90
+ if (!Array.isArray(toolCalls)) return;
91
+ // Recover names from earlier function_call items in the same transcript when a client
92
+ // re-sends tool_calls with only id/arguments (replace-style merge lost function.name).
93
+ const knownNameByCallId = new Map<string, string>();
94
+ for (const item of input) {
95
+ if (!isRec(item) || item.type !== "function_call") continue;
96
+ if (typeof item.call_id === "string" && typeof item.name === "string" && item.name.length > 0) {
97
+ knownNameByCallId.set(item.call_id, item.name);
98
+ }
99
+ }
100
+ for (const raw of toolCalls) {
101
+ if (!isRec(raw)) continue;
102
+ const fn = isRec(raw.function) ? raw.function : null;
103
+ let name = typeof fn?.name === "string" ? fn.name : typeof raw.name === "string" ? raw.name : "";
104
+ const args = typeof fn?.arguments === "string"
105
+ ? fn.arguments
106
+ : typeof raw.arguments === "string"
107
+ ? raw.arguments
108
+ : JSON.stringify(fn?.arguments ?? raw.arguments ?? {});
109
+ const callId = typeof raw.id === "string" && raw.id.length > 0
110
+ ? raw.id
111
+ : typeof raw.call_id === "string" && raw.call_id.length > 0
112
+ ? raw.call_id
113
+ : `call_${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`;
114
+ if (!name) name = knownNameByCallId.get(callId) ?? "";
115
+ if (!name) throw new ChatCompletionsRequestError("tool_calls entries require function.name");
116
+ knownNameByCallId.set(callId, name);
117
+ input.push({ type: "function_call", call_id: callId, name, arguments: args });
118
+ }
119
+ }
120
+
121
+ function toolsToResponses(tools: unknown): Rec[] | undefined {
122
+ if (!Array.isArray(tools) || tools.length === 0) return undefined;
123
+ const out: Rec[] = [];
124
+ for (const raw of tools) {
125
+ if (!isRec(raw)) continue;
126
+ if (raw.type === "function" && typeof raw.name === "string" && raw.name.length > 0) {
127
+ out.push({
128
+ type: "function",
129
+ name: raw.name,
130
+ ...(typeof raw.description === "string" ? { description: raw.description } : {}),
131
+ ...(isRec(raw.parameters) ? { parameters: raw.parameters } : {}),
132
+ ...(typeof raw.strict === "boolean" ? { strict: raw.strict } : {}),
133
+ });
134
+ continue;
135
+ }
136
+ if (raw.type === "function" && isRec(raw.function) && typeof raw.function.name === "string" && raw.function.name.length > 0) {
137
+ out.push({
138
+ type: "function",
139
+ name: raw.function.name,
140
+ ...(typeof raw.function.description === "string" ? { description: raw.function.description } : {}),
141
+ ...(isRec(raw.function.parameters) ? { parameters: raw.function.parameters } : {}),
142
+ ...(typeof raw.function.strict === "boolean" ? { strict: raw.function.strict } : {}),
143
+ });
144
+ continue;
145
+ }
146
+ if (raw.type === "web_search" || raw.type === "web_search_preview") {
147
+ out.push({ type: "web_search" });
148
+ }
149
+ }
150
+ return out.length > 0 ? out : undefined;
151
+ }
152
+
153
+ function toolChoiceToResponses(choice: unknown, body: Rec): void {
154
+ if (choice === undefined || choice === null) return;
155
+ if (choice === "auto" || choice === "none" || choice === "required") {
156
+ body.tool_choice = choice;
157
+ return;
158
+ }
159
+ if (!isRec(choice)) return;
160
+ if (choice.type === "function") {
161
+ const name = typeof choice.name === "string"
162
+ ? choice.name
163
+ : isRec(choice.function) && typeof choice.function.name === "string"
164
+ ? choice.function.name
165
+ : "";
166
+ if (!name) throw new ChatCompletionsRequestError("tool_choice.function requires a name");
167
+ body.tool_choice = { type: "function", name };
168
+ return;
169
+ }
170
+ if (isRec(choice.function) && typeof choice.function.name === "string") {
171
+ body.tool_choice = { type: "function", name: choice.function.name };
172
+ }
173
+ }
174
+
175
+ function responseFormatToText(format: unknown): Rec | undefined {
176
+ if (format === undefined) return undefined;
177
+ if (!isRec(format)) throw new ChatCompletionsRequestError("response_format must be an object");
178
+ if (format.type === "json_object") return { format: { type: "json_object" } };
179
+ if (format.type === "json_schema") {
180
+ if (!isRec(format.json_schema)) {
181
+ throw new ChatCompletionsRequestError("response_format.json_schema is required for type json_schema");
182
+ }
183
+ const schema = format.json_schema;
184
+ return {
185
+ format: {
186
+ type: "json_schema",
187
+ name: typeof schema.name === "string" ? schema.name : "response",
188
+ ...(typeof schema.description === "string" ? { description: schema.description } : {}),
189
+ ...(schema.schema !== undefined ? { schema: schema.schema } : {}),
190
+ ...(typeof schema.strict === "boolean" ? { strict: schema.strict } : {}),
191
+ },
192
+ };
193
+ }
194
+ if (format.type === "text") return undefined;
195
+ throw new ChatCompletionsRequestError(`unsupported response_format.type: ${String(format.type)}`);
196
+ }
197
+
198
+ function resolveReasoningEffort(raw: Rec): string | undefined {
199
+ if (typeof raw.reasoning_effort === "string" && OUTPUT_CONFIG_EFFORTS.has(raw.reasoning_effort)) {
200
+ return raw.reasoning_effort;
201
+ }
202
+ if (isRec(raw.reasoning) && typeof raw.reasoning.effort === "string" && OUTPUT_CONFIG_EFFORTS.has(raw.reasoning.effort)) {
203
+ return raw.reasoning.effort;
204
+ }
205
+ return undefined;
206
+ }
207
+
208
+ /**
209
+ * Translate an OpenAI Chat Completions request body into a /v1/responses request body.
210
+ * Throws ChatCompletionsRequestError (-> 400) on malformed input.
211
+ */
212
+ export function chatCompletionsToResponsesBody(raw: unknown): Rec {
213
+ if (!isRec(raw)) throw new ChatCompletionsRequestError("request body must be a JSON object");
214
+ if (typeof raw.model !== "string" || raw.model.length === 0) {
215
+ throw new ChatCompletionsRequestError("model is required");
216
+ }
217
+ if (!Array.isArray(raw.messages) || raw.messages.length === 0) {
218
+ throw new ChatCompletionsRequestError("messages must be a non-empty array");
219
+ }
220
+
221
+ const systemParts: string[] = [];
222
+ const input: Rec[] = [];
223
+
224
+ for (const msg of raw.messages) {
225
+ if (!isRec(msg)) continue;
226
+ const role = typeof msg.role === "string" ? msg.role : "";
227
+ switch (role) {
228
+ case "system":
229
+ case "developer":
230
+ pushSystemText(systemParts, msg.content);
231
+ break;
232
+ case "user": {
233
+ const blocks = userContentToBlocks(msg.content);
234
+ if (blocks.length > 0) input.push({ type: "message", role: "user", content: blocks });
235
+ break;
236
+ }
237
+ case "assistant": {
238
+ const blocks = assistantContentToBlocks(msg.content);
239
+ if (blocks.length > 0) input.push({ type: "message", role: "assistant", content: blocks });
240
+ if (msg.tool_calls !== undefined) toolCallsToItems(msg.tool_calls, input);
241
+ break;
242
+ }
243
+ case "tool": {
244
+ const callId = typeof msg.tool_call_id === "string" ? msg.tool_call_id
245
+ : typeof msg.tool_use_id === "string" ? msg.tool_use_id
246
+ : "";
247
+ if (!callId) throw new ChatCompletionsRequestError("tool messages require tool_call_id");
248
+ const output = typeof msg.content === "string" ? msg.content : contentToText(msg.content);
249
+ input.push({ type: "function_call_output", call_id: callId, output });
250
+ break;
251
+ }
252
+ default:
253
+ break;
254
+ }
255
+ }
256
+
257
+ if (input.length === 0 && systemParts.length === 0) {
258
+ throw new ChatCompletionsRequestError("messages must include at least one user/assistant/tool turn");
259
+ }
260
+
261
+ const body: Rec = {
262
+ model: raw.model,
263
+ input,
264
+ stream: raw.stream === true,
265
+ store: false,
266
+ };
267
+
268
+ if (systemParts.length > 0) body.instructions = systemParts.join("\n\n");
269
+
270
+ const tools = toolsToResponses(raw.tools);
271
+ if (tools) body.tools = tools;
272
+ toolChoiceToResponses(raw.tool_choice, body);
273
+
274
+ const maxTokens = typeof raw.max_completion_tokens === "number"
275
+ ? raw.max_completion_tokens
276
+ : typeof raw.max_tokens === "number"
277
+ ? raw.max_tokens
278
+ : undefined;
279
+ if (typeof maxTokens === "number") body.max_output_tokens = maxTokens;
280
+ if (typeof raw.temperature === "number") body.temperature = raw.temperature;
281
+ if (typeof raw.top_p === "number") body.top_p = raw.top_p;
282
+ if (raw.stop !== undefined) body.stop = raw.stop;
283
+ if (typeof raw.user === "string") body.user = raw.user;
284
+ if (typeof raw.parallel_tool_calls === "boolean") body.parallel_tool_calls = raw.parallel_tool_calls;
285
+ if (typeof raw.prompt_cache_key === "string") body.prompt_cache_key = raw.prompt_cache_key;
286
+ if (raw.metadata !== undefined) body.metadata = raw.metadata;
287
+
288
+ const effort = resolveReasoningEffort(raw);
289
+ if (effort) body.reasoning = { effort };
290
+
291
+ const text = responseFormatToText(raw.response_format);
292
+ if (text) body.text = text;
293
+
294
+ return body;
295
+ }