ampcode-connector 0.1.15 → 0.1.16
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/package.json +1 -1
- package/src/providers/codex-sse.ts +116 -19
- package/src/providers/codex.ts +41 -6
package/package.json
CHANGED
|
@@ -62,7 +62,7 @@ function createResponseTransformer(ampModel: string): (data: string) => string {
|
|
|
62
62
|
};
|
|
63
63
|
|
|
64
64
|
return (data: string): string => {
|
|
65
|
-
if (data === "[DONE]") return
|
|
65
|
+
if (data === "[DONE]") return "";
|
|
66
66
|
|
|
67
67
|
let parsed: Record<string, unknown>;
|
|
68
68
|
try {
|
|
@@ -130,18 +130,26 @@ function createResponseTransformer(ampModel: string): (data: string) => string {
|
|
|
130
130
|
return serializeFinish(state, finishReason, usage);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
// Response incomplete —
|
|
133
|
+
// Response incomplete — inspect reason to determine finish_reason
|
|
134
134
|
case "response.incomplete": {
|
|
135
135
|
const resp = parsed.response as Record<string, unknown>;
|
|
136
136
|
const usage = extractUsage(resp?.usage as Record<string, unknown> | undefined);
|
|
137
|
-
|
|
137
|
+
const finishReason = incompleteReason(resp);
|
|
138
|
+
return serializeFinish(state, finishReason, usage);
|
|
138
139
|
}
|
|
139
140
|
|
|
140
|
-
// Response failed — emit
|
|
141
|
+
// Response failed — emit error content so the client sees the failure
|
|
141
142
|
case "response.failed": {
|
|
142
143
|
const resp = parsed.response as Record<string, unknown>;
|
|
143
144
|
const usage = extractUsage(resp?.usage as Record<string, unknown> | undefined);
|
|
144
|
-
|
|
145
|
+
const errorMsg = extractErrorMessage(resp);
|
|
146
|
+
let chunks = "";
|
|
147
|
+
if (errorMsg) {
|
|
148
|
+
chunks = serialize(state, { role: "assistant", content: `[Error] ${errorMsg}` });
|
|
149
|
+
chunks += "\n\n";
|
|
150
|
+
}
|
|
151
|
+
chunks += serializeFinish(state, "stop", usage);
|
|
152
|
+
return chunks;
|
|
145
153
|
}
|
|
146
154
|
|
|
147
155
|
// Reasoning/thinking delta — emit as reasoning_content (separate from content)
|
|
@@ -191,6 +199,28 @@ function serializeFinish(state: TransformState, finishReason: string, usage?: Us
|
|
|
191
199
|
return JSON.stringify(chunk);
|
|
192
200
|
}
|
|
193
201
|
|
|
202
|
+
/** Map Responses API incomplete reason → Chat Completions finish_reason. */
|
|
203
|
+
function incompleteReason(resp: Record<string, unknown> | undefined): string {
|
|
204
|
+
if (!resp) return "length";
|
|
205
|
+
const reason = resp.incomplete_details as Record<string, unknown> | undefined;
|
|
206
|
+
const type = reason?.reason as string | undefined;
|
|
207
|
+
if (type === "max_output_tokens" || type === "max_tokens") return "length";
|
|
208
|
+
if (type === "content_filter") return "content_filter";
|
|
209
|
+
return "length";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Extract a human-readable error message from a failed response. */
|
|
213
|
+
function extractErrorMessage(resp: Record<string, unknown> | undefined): string | null {
|
|
214
|
+
if (!resp) return null;
|
|
215
|
+
const error = resp.error as Record<string, unknown> | undefined;
|
|
216
|
+
if (!error) return null;
|
|
217
|
+
const message = error.message as string | undefined;
|
|
218
|
+
const code = error.code as string | undefined;
|
|
219
|
+
if (message) return code ? `${code}: ${message}` : message;
|
|
220
|
+
if (code) return code;
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
194
224
|
function extractUsage(raw: Record<string, unknown> | undefined): Usage | undefined {
|
|
195
225
|
if (!raw) return undefined;
|
|
196
226
|
const input = (raw.input_tokens as number) ?? 0;
|
|
@@ -206,6 +236,15 @@ function extractUsage(raw: Record<string, unknown> | undefined): Usage | undefin
|
|
|
206
236
|
};
|
|
207
237
|
}
|
|
208
238
|
|
|
239
|
+
const FORWARDED_HEADERS = [
|
|
240
|
+
"x-request-id",
|
|
241
|
+
"request-id",
|
|
242
|
+
"x-ratelimit-limit-requests",
|
|
243
|
+
"x-ratelimit-remaining-requests",
|
|
244
|
+
"x-ratelimit-limit-tokens",
|
|
245
|
+
"x-ratelimit-remaining-tokens",
|
|
246
|
+
] as const;
|
|
247
|
+
|
|
209
248
|
/** Wrap a Codex SSE response with the Responses → Chat Completions transformer.
|
|
210
249
|
* Strips Responses API event names so output looks like standard Chat Completions SSE. */
|
|
211
250
|
export function transformCodexResponse(response: Response, ampModel: string): Response {
|
|
@@ -214,14 +253,17 @@ export function transformCodexResponse(response: Response, ampModel: string): Re
|
|
|
214
253
|
const transformer = createResponseTransformer(ampModel);
|
|
215
254
|
const body = transformStream(response.body, transformer);
|
|
216
255
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
256
|
+
const headers: Record<string, string> = {
|
|
257
|
+
"Content-Type": "text/event-stream",
|
|
258
|
+
"Cache-Control": "no-cache",
|
|
259
|
+
Connection: "keep-alive",
|
|
260
|
+
};
|
|
261
|
+
for (const name of FORWARDED_HEADERS) {
|
|
262
|
+
const value = response.headers.get(name);
|
|
263
|
+
if (value) headers[name] = value;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return new Response(body, { status: response.status, headers });
|
|
225
267
|
}
|
|
226
268
|
|
|
227
269
|
/** Buffer a Codex SSE response into a single Chat Completions JSON response.
|
|
@@ -324,32 +366,87 @@ export async function bufferCodexResponse(response: Response, ampModel: string):
|
|
|
324
366
|
case "response.incomplete": {
|
|
325
367
|
const resp = parsed.response as Record<string, unknown>;
|
|
326
368
|
usage = extractUsage(resp?.usage as Record<string, unknown> | undefined);
|
|
327
|
-
finishReason =
|
|
369
|
+
finishReason = incompleteReason(resp);
|
|
328
370
|
break;
|
|
329
371
|
}
|
|
330
372
|
|
|
331
373
|
case "response.failed": {
|
|
332
374
|
const resp = parsed.response as Record<string, unknown>;
|
|
333
375
|
usage = extractUsage(resp?.usage as Record<string, unknown> | undefined);
|
|
376
|
+
const errorMsg = extractErrorMessage(resp);
|
|
377
|
+
if (errorMsg) content += `[Error] ${errorMsg}`;
|
|
334
378
|
break;
|
|
335
379
|
}
|
|
336
380
|
}
|
|
337
381
|
}
|
|
338
382
|
}
|
|
339
383
|
|
|
340
|
-
// Process remaining buffer
|
|
384
|
+
// Process remaining buffer — reuse the same event handling as main loop
|
|
341
385
|
if (sseBuffer.trim()) {
|
|
342
386
|
for (const chunk of sse.parse(sseBuffer)) {
|
|
343
387
|
if (chunk.data === "[DONE]") continue;
|
|
388
|
+
|
|
389
|
+
let parsed: Record<string, unknown>;
|
|
344
390
|
try {
|
|
345
|
-
|
|
346
|
-
|
|
391
|
+
parsed = JSON.parse(chunk.data) as Record<string, unknown>;
|
|
392
|
+
} catch {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const eventType = parsed.type as string | undefined;
|
|
397
|
+
if (!eventType) continue;
|
|
398
|
+
|
|
399
|
+
switch (eventType) {
|
|
400
|
+
case "response.output_text.delta": {
|
|
401
|
+
const delta = parsed.delta as string;
|
|
402
|
+
if (delta) content += delta;
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
case "response.reasoning_summary_text.delta": {
|
|
406
|
+
const delta = parsed.delta as string;
|
|
407
|
+
if (delta) reasoningContent += delta;
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
case "response.output_item.added": {
|
|
411
|
+
const item = parsed.item as Record<string, unknown>;
|
|
412
|
+
if (item?.type === "function_call") {
|
|
413
|
+
const callId = item.call_id as string;
|
|
414
|
+
const name = item.name as string;
|
|
415
|
+
const idx = state.toolCallIndex++;
|
|
416
|
+
state.toolCallIds.set(callId, idx);
|
|
417
|
+
toolCalls.set(idx, { id: callId, type: "function", function: { name, arguments: "" } });
|
|
418
|
+
}
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
case "response.function_call_arguments.delta": {
|
|
422
|
+
const delta = parsed.delta as string;
|
|
423
|
+
const callId = parsed.call_id as string | undefined;
|
|
424
|
+
if (delta) {
|
|
425
|
+
const idx = callId ? (state.toolCallIds.get(callId) ?? 0) : 0;
|
|
426
|
+
const tc = toolCalls.get(idx);
|
|
427
|
+
if (tc) tc.function.arguments += delta;
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
case "response.completed": {
|
|
347
432
|
const resp = parsed.response as Record<string, unknown>;
|
|
348
433
|
usage = extractUsage(resp?.usage as Record<string, unknown> | undefined);
|
|
349
434
|
finishReason = state.toolCallIndex > 0 ? "tool_calls" : "stop";
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
case "response.incomplete": {
|
|
438
|
+
const resp = parsed.response as Record<string, unknown>;
|
|
439
|
+
usage = extractUsage(resp?.usage as Record<string, unknown> | undefined);
|
|
440
|
+
finishReason = incompleteReason(resp);
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
case "response.failed": {
|
|
444
|
+
const resp = parsed.response as Record<string, unknown>;
|
|
445
|
+
usage = extractUsage(resp?.usage as Record<string, unknown> | undefined);
|
|
446
|
+
const errorMsg = extractErrorMessage(resp);
|
|
447
|
+
if (errorMsg) content += `[Error] ${errorMsg}`;
|
|
448
|
+
break;
|
|
350
449
|
}
|
|
351
|
-
} catch {
|
|
352
|
-
// skip
|
|
353
450
|
}
|
|
354
451
|
}
|
|
355
452
|
}
|
package/src/providers/codex.ts
CHANGED
|
@@ -135,16 +135,22 @@ function transformForCodex(
|
|
|
135
135
|
fixOrphanOutputs(parsed.input as Record<string, unknown>[]);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
// Reasoning config — defaults match reference behavior
|
|
138
|
+
// Reasoning config — merge with caller-provided values, defaults match reference behavior
|
|
139
139
|
const model = (parsed.model as string) ?? "";
|
|
140
|
+
const existingReasoning = (parsed.reasoning as Record<string, unknown>) ?? {};
|
|
140
141
|
parsed.reasoning = {
|
|
141
|
-
effort: clampReasoningEffort(model, "high"),
|
|
142
|
-
summary: "auto",
|
|
142
|
+
effort: clampReasoningEffort(model, (existingReasoning.effort as string) ?? "high"),
|
|
143
|
+
summary: existingReasoning.summary ?? "auto",
|
|
143
144
|
};
|
|
144
145
|
|
|
145
|
-
parsed.text
|
|
146
|
+
const existingText = (parsed.text as Record<string, unknown>) ?? {};
|
|
147
|
+
parsed.text = { ...existingText, verbosity: existingText.verbosity ?? "medium" };
|
|
146
148
|
|
|
147
|
-
parsed.include
|
|
149
|
+
const existingInclude = Array.isArray(parsed.include) ? (parsed.include as string[]) : [];
|
|
150
|
+
if (!existingInclude.includes("reasoning.encrypted_content")) {
|
|
151
|
+
existingInclude.push("reasoning.encrypted_content");
|
|
152
|
+
}
|
|
153
|
+
parsed.include = existingInclude;
|
|
148
154
|
|
|
149
155
|
if (promptCacheKey) {
|
|
150
156
|
parsed.prompt_cache_key = promptCacheKey;
|
|
@@ -165,6 +171,23 @@ function transformForCodex(
|
|
|
165
171
|
delete parsed.logit_bias;
|
|
166
172
|
delete parsed.response_format;
|
|
167
173
|
|
|
174
|
+
// Normalize tools[] for Responses API: flatten function.{name,description,parameters,strict} to top-level
|
|
175
|
+
if (Array.isArray(parsed.tools)) {
|
|
176
|
+
parsed.tools = (parsed.tools as Record<string, unknown>[]).map((tool) => {
|
|
177
|
+
if (tool.type === "function" && tool.function && typeof tool.function === "object") {
|
|
178
|
+
const fn = tool.function as Record<string, unknown>;
|
|
179
|
+
return {
|
|
180
|
+
type: "function",
|
|
181
|
+
name: fn.name,
|
|
182
|
+
description: fn.description,
|
|
183
|
+
parameters: fn.parameters,
|
|
184
|
+
...(fn.strict !== undefined ? { strict: fn.strict } : {}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return tool;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
168
191
|
// Normalize tool_choice for Responses API
|
|
169
192
|
if (parsed.tool_choice !== undefined && parsed.tool_choice !== null) {
|
|
170
193
|
if (typeof parsed.tool_choice === "string") {
|
|
@@ -236,7 +259,7 @@ function convertMessages(messages: ChatMessage[]): { instructions: string | null
|
|
|
236
259
|
input.push({
|
|
237
260
|
type: "function_call_output",
|
|
238
261
|
call_id: msg.tool_call_id,
|
|
239
|
-
output:
|
|
262
|
+
output: stringifyContent(msg.content),
|
|
240
263
|
});
|
|
241
264
|
break;
|
|
242
265
|
}
|
|
@@ -265,6 +288,18 @@ function convertUserContent(content: unknown): unknown[] {
|
|
|
265
288
|
return [{ type: "input_text", text: String(content) }];
|
|
266
289
|
}
|
|
267
290
|
|
|
291
|
+
/** Convert content to string, with JSON fallback for non-text values. */
|
|
292
|
+
function stringifyContent(content: unknown): string {
|
|
293
|
+
if (typeof content === "string") return content;
|
|
294
|
+
const text = textOf(content);
|
|
295
|
+
if (text !== null) return text;
|
|
296
|
+
try {
|
|
297
|
+
return JSON.stringify(content);
|
|
298
|
+
} catch {
|
|
299
|
+
return String(content ?? "");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
268
303
|
/** Extract text from content (string or array). */
|
|
269
304
|
function textOf(content: unknown): string | null {
|
|
270
305
|
if (typeof content === "string") return content;
|