ampcode-connector 0.1.11 → 0.1.13
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/config/config.ts +3 -0
- package/src/index.ts +1 -1
- package/src/providers/codex-sse.ts +175 -2
- package/src/providers/codex.ts +4 -1
- package/src/server/server.ts +2 -2
package/package.json
CHANGED
package/src/config/config.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { LogLevel } from "../utils/logger.ts";
|
|
|
7
7
|
import { logger } from "../utils/logger.ts";
|
|
8
8
|
|
|
9
9
|
export interface ProxyConfig {
|
|
10
|
+
hostname: string;
|
|
10
11
|
port: number;
|
|
11
12
|
ampUpstreamUrl: string;
|
|
12
13
|
ampApiKey?: string;
|
|
@@ -20,6 +21,7 @@ export interface ProxyConfig {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
const DEFAULTS: ProxyConfig = {
|
|
24
|
+
hostname: "localhost",
|
|
23
25
|
port: 8765,
|
|
24
26
|
ampUpstreamUrl: DEFAULT_AMP_UPSTREAM_URL,
|
|
25
27
|
logLevel: "info",
|
|
@@ -44,6 +46,7 @@ export async function loadConfig(): Promise<ProxyConfig> {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
return {
|
|
49
|
+
hostname: asString(file?.hostname) ?? process.env.HOST ?? DEFAULTS.hostname,
|
|
47
50
|
port,
|
|
48
51
|
ampUpstreamUrl: asString(file?.ampUpstreamUrl) ?? DEFAULTS.ampUpstreamUrl,
|
|
49
52
|
ampApiKey: apiKey,
|
package/src/index.ts
CHANGED
|
@@ -59,7 +59,7 @@ function banner(config: ProxyConfig): void {
|
|
|
59
59
|
|
|
60
60
|
line();
|
|
61
61
|
line(` ${s.bold}ampcode-connector${s.reset}`);
|
|
62
|
-
line(` ${s.dim}http
|
|
62
|
+
line(` ${s.dim}http://${config.hostname}:${config.port}${s.reset}`);
|
|
63
63
|
line();
|
|
64
64
|
|
|
65
65
|
for (const p of providers) {
|
|
@@ -23,6 +23,7 @@ interface Choice {
|
|
|
23
23
|
interface Delta {
|
|
24
24
|
role?: string;
|
|
25
25
|
content?: string;
|
|
26
|
+
reasoning_content?: string;
|
|
26
27
|
tool_calls?: ToolCallDelta[];
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -38,6 +39,7 @@ interface Usage {
|
|
|
38
39
|
completion_tokens: number;
|
|
39
40
|
total_tokens: number;
|
|
40
41
|
prompt_tokens_details?: { cached_tokens: number };
|
|
42
|
+
completion_tokens_details?: { reasoning_tokens: number };
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
interface TransformState {
|
|
@@ -128,10 +130,24 @@ function createResponseTransformer(ampModel: string): (data: string) => string {
|
|
|
128
130
|
return serializeFinish(state, finishReason, usage);
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
//
|
|
133
|
+
// Response incomplete — emit finish_reason "length" + usage
|
|
134
|
+
case "response.incomplete": {
|
|
135
|
+
const resp = parsed.response as Record<string, unknown>;
|
|
136
|
+
const usage = extractUsage(resp?.usage as Record<string, unknown> | undefined);
|
|
137
|
+
return serializeFinish(state, "length", usage);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Response failed — emit finish_reason "stop" (error)
|
|
141
|
+
case "response.failed": {
|
|
142
|
+
const resp = parsed.response as Record<string, unknown>;
|
|
143
|
+
const usage = extractUsage(resp?.usage as Record<string, unknown> | undefined);
|
|
144
|
+
return serializeFinish(state, "stop", usage);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Reasoning/thinking delta — emit as reasoning_content (separate from content)
|
|
132
148
|
case "response.reasoning_summary_text.delta": {
|
|
133
149
|
const delta = parsed.delta as string;
|
|
134
|
-
if (delta) return serialize(state, {
|
|
150
|
+
if (delta) return serialize(state, { reasoning_content: delta });
|
|
135
151
|
return "";
|
|
136
152
|
}
|
|
137
153
|
|
|
@@ -180,11 +196,13 @@ function extractUsage(raw: Record<string, unknown> | undefined): Usage | undefin
|
|
|
180
196
|
const input = (raw.input_tokens as number) ?? 0;
|
|
181
197
|
const output = (raw.output_tokens as number) ?? 0;
|
|
182
198
|
const cached = (raw.input_tokens_details as Record<string, unknown>)?.cached_tokens as number | undefined;
|
|
199
|
+
const reasoning = (raw.output_tokens_details as Record<string, unknown>)?.reasoning_tokens as number | undefined;
|
|
183
200
|
return {
|
|
184
201
|
prompt_tokens: input,
|
|
185
202
|
completion_tokens: output,
|
|
186
203
|
total_tokens: input + output,
|
|
187
204
|
...(cached !== undefined ? { prompt_tokens_details: { cached_tokens: cached } } : {}),
|
|
205
|
+
...(reasoning !== undefined ? { completion_tokens_details: { reasoning_tokens: reasoning } } : {}),
|
|
188
206
|
};
|
|
189
207
|
}
|
|
190
208
|
|
|
@@ -206,6 +224,161 @@ export function transformCodexResponse(response: Response, ampModel: string): Re
|
|
|
206
224
|
});
|
|
207
225
|
}
|
|
208
226
|
|
|
227
|
+
/** Buffer a Codex SSE response into a single Chat Completions JSON response.
|
|
228
|
+
* Used when the client requests stream: false but the backend forces streaming. */
|
|
229
|
+
export async function bufferCodexResponse(response: Response, ampModel: string): Promise<Response> {
|
|
230
|
+
if (!response.body) return response;
|
|
231
|
+
|
|
232
|
+
const state: TransformState = {
|
|
233
|
+
responseId: "",
|
|
234
|
+
model: ampModel,
|
|
235
|
+
created: Math.floor(Date.now() / 1000),
|
|
236
|
+
toolCallIndex: 0,
|
|
237
|
+
toolCallIds: new Map(),
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
let content = "";
|
|
241
|
+
let reasoningContent = "";
|
|
242
|
+
const toolCalls: Map<number, { id: string; type: string; function: { name: string; arguments: string } }> = new Map();
|
|
243
|
+
let finishReason = "stop";
|
|
244
|
+
let usage: Usage | undefined;
|
|
245
|
+
|
|
246
|
+
const decoder = new TextDecoder();
|
|
247
|
+
let sseBuffer = "";
|
|
248
|
+
|
|
249
|
+
const reader = response.body.getReader();
|
|
250
|
+
for (;;) {
|
|
251
|
+
const { done, value } = await reader.read();
|
|
252
|
+
if (done) break;
|
|
253
|
+
|
|
254
|
+
sseBuffer += decoder.decode(value, { stream: true }).replaceAll("\r\n", "\n");
|
|
255
|
+
const boundary = sseBuffer.lastIndexOf("\n\n");
|
|
256
|
+
if (boundary === -1) continue;
|
|
257
|
+
|
|
258
|
+
const complete = sseBuffer.slice(0, boundary + 2);
|
|
259
|
+
sseBuffer = sseBuffer.slice(boundary + 2);
|
|
260
|
+
|
|
261
|
+
for (const chunk of sse.parse(complete)) {
|
|
262
|
+
if (chunk.data === "[DONE]") continue;
|
|
263
|
+
|
|
264
|
+
let parsed: Record<string, unknown>;
|
|
265
|
+
try {
|
|
266
|
+
parsed = JSON.parse(chunk.data) as Record<string, unknown>;
|
|
267
|
+
} catch {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const eventType = parsed.type as string | undefined;
|
|
272
|
+
if (!eventType) continue;
|
|
273
|
+
|
|
274
|
+
if (eventType === "response.created") {
|
|
275
|
+
const resp = parsed.response as Record<string, unknown>;
|
|
276
|
+
state.responseId = (resp?.id as string) ?? state.responseId;
|
|
277
|
+
state.created = (resp?.created_at as number) ?? state.created;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
switch (eventType) {
|
|
282
|
+
case "response.output_text.delta": {
|
|
283
|
+
const delta = parsed.delta as string;
|
|
284
|
+
if (delta) content += delta;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
case "response.reasoning_summary_text.delta": {
|
|
289
|
+
const delta = parsed.delta as string;
|
|
290
|
+
if (delta) reasoningContent += delta;
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
case "response.output_item.added": {
|
|
295
|
+
const item = parsed.item as Record<string, unknown>;
|
|
296
|
+
if (item?.type === "function_call") {
|
|
297
|
+
const callId = item.call_id as string;
|
|
298
|
+
const name = item.name as string;
|
|
299
|
+
const idx = state.toolCallIndex++;
|
|
300
|
+
state.toolCallIds.set(callId, idx);
|
|
301
|
+
toolCalls.set(idx, { id: callId, type: "function", function: { name, arguments: "" } });
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
case "response.function_call_arguments.delta": {
|
|
307
|
+
const delta = parsed.delta as string;
|
|
308
|
+
const callId = parsed.call_id as string | undefined;
|
|
309
|
+
if (delta) {
|
|
310
|
+
const idx = callId ? (state.toolCallIds.get(callId) ?? 0) : 0;
|
|
311
|
+
const tc = toolCalls.get(idx);
|
|
312
|
+
if (tc) tc.function.arguments += delta;
|
|
313
|
+
}
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
case "response.completed": {
|
|
318
|
+
const resp = parsed.response as Record<string, unknown>;
|
|
319
|
+
usage = extractUsage(resp?.usage as Record<string, unknown> | undefined);
|
|
320
|
+
finishReason = state.toolCallIndex > 0 ? "tool_calls" : "stop";
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
case "response.incomplete": {
|
|
325
|
+
const resp = parsed.response as Record<string, unknown>;
|
|
326
|
+
usage = extractUsage(resp?.usage as Record<string, unknown> | undefined);
|
|
327
|
+
finishReason = "length";
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
case "response.failed": {
|
|
332
|
+
const resp = parsed.response as Record<string, unknown>;
|
|
333
|
+
usage = extractUsage(resp?.usage as Record<string, unknown> | undefined);
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Process remaining buffer
|
|
341
|
+
if (sseBuffer.trim()) {
|
|
342
|
+
for (const chunk of sse.parse(sseBuffer)) {
|
|
343
|
+
if (chunk.data === "[DONE]") continue;
|
|
344
|
+
try {
|
|
345
|
+
const parsed = JSON.parse(chunk.data) as Record<string, unknown>;
|
|
346
|
+
if (parsed.type === "response.completed") {
|
|
347
|
+
const resp = parsed.response as Record<string, unknown>;
|
|
348
|
+
usage = extractUsage(resp?.usage as Record<string, unknown> | undefined);
|
|
349
|
+
finishReason = state.toolCallIndex > 0 ? "tool_calls" : "stop";
|
|
350
|
+
}
|
|
351
|
+
} catch {
|
|
352
|
+
// skip
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const message: Record<string, unknown> = { role: "assistant", content: content || null };
|
|
358
|
+
if (reasoningContent) {
|
|
359
|
+
message.reasoning_content = reasoningContent;
|
|
360
|
+
}
|
|
361
|
+
if (toolCalls.size > 0) {
|
|
362
|
+
message.tool_calls = Array.from(toolCalls.entries())
|
|
363
|
+
.sort(([a], [b]) => a - b)
|
|
364
|
+
.map(([, tc]) => tc);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const result = {
|
|
368
|
+
id: `chatcmpl-${state.responseId}`,
|
|
369
|
+
object: "chat.completion",
|
|
370
|
+
created: state.created,
|
|
371
|
+
model: state.model,
|
|
372
|
+
choices: [{ index: 0, message, finish_reason: finishReason }],
|
|
373
|
+
...(usage ? { usage } : {}),
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
return new Response(JSON.stringify(result), {
|
|
377
|
+
status: 200,
|
|
378
|
+
headers: { "Content-Type": "application/json" },
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
209
382
|
/** Custom SSE transform that strips event names (Chat Completions doesn't use them). */
|
|
210
383
|
function transformStream(source: ReadableStream<Uint8Array>, fn: (data: string) => string): ReadableStream<Uint8Array> {
|
|
211
384
|
const decoder = new TextDecoder();
|
package/src/providers/codex.ts
CHANGED
|
@@ -10,7 +10,7 @@ import * as store from "../auth/store.ts";
|
|
|
10
10
|
import { CODEX_BASE_URL, codexHeaders, codexHeaderValues, codexPathMap } from "../constants.ts";
|
|
11
11
|
import { fromBase64url } from "../utils/encoding.ts";
|
|
12
12
|
import type { Provider } from "./base.ts";
|
|
13
|
-
import { transformCodexResponse } from "./codex-sse.ts";
|
|
13
|
+
import { bufferCodexResponse, transformCodexResponse } from "./codex-sse.ts";
|
|
14
14
|
import { denied, forward } from "./forward.ts";
|
|
15
15
|
|
|
16
16
|
const DEFAULT_INSTRUCTIONS = "You are an expert coding assistant.";
|
|
@@ -60,6 +60,9 @@ export const provider: Provider = {
|
|
|
60
60
|
|
|
61
61
|
// Transform Responses API SSE → Chat Completions SSE when original was messages[] format
|
|
62
62
|
if (needsResponseTransform && response.ok) {
|
|
63
|
+
if (!body.stream) {
|
|
64
|
+
return bufferCodexResponse(response, ampModel);
|
|
65
|
+
}
|
|
63
66
|
return transformCodexResponse(response, ampModel);
|
|
64
67
|
}
|
|
65
68
|
return response;
|
package/src/server/server.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { type ParsedBody, parseBody } from "./body.ts";
|
|
|
17
17
|
export function startServer(config: ProxyConfig): ReturnType<typeof Bun.serve> {
|
|
18
18
|
const server = Bun.serve({
|
|
19
19
|
port: config.port,
|
|
20
|
-
hostname:
|
|
20
|
+
hostname: config.hostname,
|
|
21
21
|
idleTimeout: 255, // seconds — LLM streaming responses can take minutes
|
|
22
22
|
|
|
23
23
|
async fetch(req) {
|
|
@@ -38,7 +38,7 @@ export function startServer(config: ProxyConfig): ReturnType<typeof Bun.serve> {
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
affinity.startCleanup();
|
|
41
|
-
logger.info(`ampcode-connector listening on http
|
|
41
|
+
logger.info(`ampcode-connector listening on http://${config.hostname}:${config.port}`);
|
|
42
42
|
|
|
43
43
|
const shutdown = () => {
|
|
44
44
|
logger.info("Shutting down...");
|