llm-cli-gateway 2.7.0 → 2.8.0
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/CHANGELOG.md +37 -0
- package/README.md +5 -1
- package/dist/acp/client.d.ts +78 -0
- package/dist/acp/client.js +201 -0
- package/dist/acp/errors.d.ts +63 -0
- package/dist/acp/errors.js +139 -0
- package/dist/acp/json-rpc-stdio.d.ts +71 -0
- package/dist/acp/json-rpc-stdio.js +375 -0
- package/dist/acp/process-manager.d.ts +66 -0
- package/dist/acp/process-manager.js +364 -0
- package/dist/acp/provider-registry.d.ts +24 -0
- package/dist/acp/provider-registry.js +82 -0
- package/dist/acp/types.d.ts +557 -0
- package/dist/acp/types.js +335 -0
- package/dist/async-job-manager.d.ts +2 -0
- package/dist/async-job-manager.js +45 -16
- package/dist/cache-stats.js +17 -10
- package/dist/codex-json-parser.d.ts +3 -0
- package/dist/codex-json-parser.js +17 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.js +119 -0
- package/dist/http-transport.js +2 -1
- package/dist/index.js +18 -15
- package/dist/pricing.d.ts +1 -1
- package/dist/pricing.js +67 -2
- package/dist/provider-tool-capabilities.d.ts +38 -0
- package/dist/provider-tool-capabilities.js +142 -0
- package/dist/request-context.d.ts +1 -0
- package/dist/request-helpers.d.ts +4 -4
- package/dist/upstream-contracts.d.ts +27 -0
- package/dist/upstream-contracts.js +131 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { z } from "zod/v3";
|
|
2
|
+
import { AcpProtocolError } from "./errors.js";
|
|
3
|
+
export const ProtocolVersionSchema = z.number().int().nonnegative();
|
|
4
|
+
export const SessionIdSchema = z.string().min(1);
|
|
5
|
+
const MetaSchema = z.record(z.unknown()).optional();
|
|
6
|
+
const KNOWN_CONTENT_BLOCK_TYPES = new Set(["text", "image", "audio", "resource_link", "resource"]);
|
|
7
|
+
export const ContentBlockSchema = z.lazy(() => z.union([
|
|
8
|
+
z.object({ type: z.literal("text"), text: z.string(), _meta: MetaSchema }).passthrough(),
|
|
9
|
+
z
|
|
10
|
+
.object({
|
|
11
|
+
type: z.literal("image"),
|
|
12
|
+
data: z.string(),
|
|
13
|
+
mimeType: z.string(),
|
|
14
|
+
_meta: MetaSchema,
|
|
15
|
+
})
|
|
16
|
+
.passthrough(),
|
|
17
|
+
z
|
|
18
|
+
.object({
|
|
19
|
+
type: z.literal("audio"),
|
|
20
|
+
data: z.string(),
|
|
21
|
+
mimeType: z.string(),
|
|
22
|
+
_meta: MetaSchema,
|
|
23
|
+
})
|
|
24
|
+
.passthrough(),
|
|
25
|
+
z
|
|
26
|
+
.object({
|
|
27
|
+
type: z.literal("resource_link"),
|
|
28
|
+
uri: z.string(),
|
|
29
|
+
_meta: MetaSchema,
|
|
30
|
+
})
|
|
31
|
+
.passthrough(),
|
|
32
|
+
z.object({ type: z.literal("resource"), _meta: MetaSchema }).passthrough(),
|
|
33
|
+
z
|
|
34
|
+
.object({
|
|
35
|
+
type: z.string().refine(t => !KNOWN_CONTENT_BLOCK_TYPES.has(t), {
|
|
36
|
+
message: "known content block type missing required fields",
|
|
37
|
+
}),
|
|
38
|
+
})
|
|
39
|
+
.passthrough(),
|
|
40
|
+
]));
|
|
41
|
+
export const ClientFsCapabilitiesSchema = z
|
|
42
|
+
.object({
|
|
43
|
+
readTextFile: z.boolean().optional(),
|
|
44
|
+
writeTextFile: z.boolean().optional(),
|
|
45
|
+
})
|
|
46
|
+
.passthrough();
|
|
47
|
+
export const ClientCapabilitiesSchema = z
|
|
48
|
+
.object({
|
|
49
|
+
fs: ClientFsCapabilitiesSchema.optional(),
|
|
50
|
+
terminal: z.boolean().optional(),
|
|
51
|
+
})
|
|
52
|
+
.passthrough();
|
|
53
|
+
export const InitializeRequestSchema = z
|
|
54
|
+
.object({
|
|
55
|
+
protocolVersion: ProtocolVersionSchema,
|
|
56
|
+
clientCapabilities: ClientCapabilitiesSchema.optional(),
|
|
57
|
+
_meta: MetaSchema,
|
|
58
|
+
})
|
|
59
|
+
.passthrough();
|
|
60
|
+
export const InitializeResponseSchema = z
|
|
61
|
+
.object({
|
|
62
|
+
protocolVersion: ProtocolVersionSchema,
|
|
63
|
+
agentCapabilities: z.record(z.unknown()).optional(),
|
|
64
|
+
agentInfo: z
|
|
65
|
+
.object({
|
|
66
|
+
name: z.string().optional(),
|
|
67
|
+
version: z.string().optional(),
|
|
68
|
+
})
|
|
69
|
+
.passthrough()
|
|
70
|
+
.optional(),
|
|
71
|
+
authMethods: z.array(z.record(z.unknown())).optional(),
|
|
72
|
+
_meta: MetaSchema,
|
|
73
|
+
})
|
|
74
|
+
.passthrough();
|
|
75
|
+
export const McpServerSchema = z.record(z.unknown());
|
|
76
|
+
export const SessionNewRequestSchema = z
|
|
77
|
+
.object({
|
|
78
|
+
cwd: z.string().min(1),
|
|
79
|
+
mcpServers: z.array(McpServerSchema).default([]),
|
|
80
|
+
_meta: MetaSchema,
|
|
81
|
+
})
|
|
82
|
+
.passthrough();
|
|
83
|
+
export const SessionNewResponseSchema = z
|
|
84
|
+
.object({
|
|
85
|
+
sessionId: SessionIdSchema,
|
|
86
|
+
modes: z.record(z.unknown()).optional(),
|
|
87
|
+
_meta: MetaSchema,
|
|
88
|
+
})
|
|
89
|
+
.passthrough();
|
|
90
|
+
export const SessionLoadRequestSchema = z
|
|
91
|
+
.object({
|
|
92
|
+
sessionId: SessionIdSchema,
|
|
93
|
+
cwd: z.string().min(1),
|
|
94
|
+
mcpServers: z.array(McpServerSchema).default([]),
|
|
95
|
+
_meta: MetaSchema,
|
|
96
|
+
})
|
|
97
|
+
.passthrough();
|
|
98
|
+
export const SessionLoadResponseSchema = z
|
|
99
|
+
.object({
|
|
100
|
+
modes: z.record(z.unknown()).optional(),
|
|
101
|
+
_meta: MetaSchema,
|
|
102
|
+
})
|
|
103
|
+
.passthrough();
|
|
104
|
+
export const SessionPromptRequestSchema = z
|
|
105
|
+
.object({
|
|
106
|
+
sessionId: SessionIdSchema,
|
|
107
|
+
prompt: z.array(ContentBlockSchema).min(1),
|
|
108
|
+
_meta: MetaSchema,
|
|
109
|
+
})
|
|
110
|
+
.passthrough();
|
|
111
|
+
export const STOP_REASONS = [
|
|
112
|
+
"end_turn",
|
|
113
|
+
"max_tokens",
|
|
114
|
+
"max_turn_requests",
|
|
115
|
+
"refusal",
|
|
116
|
+
"cancelled",
|
|
117
|
+
];
|
|
118
|
+
export const SessionPromptResponseSchema = z
|
|
119
|
+
.object({
|
|
120
|
+
stopReason: z.string().min(1),
|
|
121
|
+
_meta: MetaSchema,
|
|
122
|
+
})
|
|
123
|
+
.passthrough();
|
|
124
|
+
export const SessionCancelNotificationSchema = z
|
|
125
|
+
.object({
|
|
126
|
+
sessionId: SessionIdSchema,
|
|
127
|
+
_meta: MetaSchema,
|
|
128
|
+
})
|
|
129
|
+
.passthrough();
|
|
130
|
+
const MessageChunkUpdate = (variant) => z
|
|
131
|
+
.object({
|
|
132
|
+
sessionUpdate: z.literal(variant),
|
|
133
|
+
content: ContentBlockSchema,
|
|
134
|
+
messageId: z.string().nullish(),
|
|
135
|
+
_meta: MetaSchema,
|
|
136
|
+
})
|
|
137
|
+
.passthrough();
|
|
138
|
+
const ToolCallUpdate = z
|
|
139
|
+
.object({
|
|
140
|
+
sessionUpdate: z.literal("tool_call"),
|
|
141
|
+
toolCallId: z.string().min(1),
|
|
142
|
+
title: z.string(),
|
|
143
|
+
status: z.string().optional(),
|
|
144
|
+
kind: z.string().optional(),
|
|
145
|
+
_meta: MetaSchema,
|
|
146
|
+
})
|
|
147
|
+
.passthrough();
|
|
148
|
+
const ToolCallUpdateUpdate = z
|
|
149
|
+
.object({
|
|
150
|
+
sessionUpdate: z.literal("tool_call_update"),
|
|
151
|
+
toolCallId: z.string().min(1),
|
|
152
|
+
status: z.string().nullish(),
|
|
153
|
+
title: z.string().nullish(),
|
|
154
|
+
_meta: MetaSchema,
|
|
155
|
+
})
|
|
156
|
+
.passthrough();
|
|
157
|
+
const PlanUpdate = z
|
|
158
|
+
.object({
|
|
159
|
+
sessionUpdate: z.literal("plan"),
|
|
160
|
+
entries: z.array(z.record(z.unknown())),
|
|
161
|
+
_meta: MetaSchema,
|
|
162
|
+
})
|
|
163
|
+
.passthrough();
|
|
164
|
+
const AvailableCommandsUpdate = z
|
|
165
|
+
.object({
|
|
166
|
+
sessionUpdate: z.literal("available_commands_update"),
|
|
167
|
+
availableCommands: z.array(z.record(z.unknown())),
|
|
168
|
+
_meta: MetaSchema,
|
|
169
|
+
})
|
|
170
|
+
.passthrough();
|
|
171
|
+
const CurrentModeUpdate = z
|
|
172
|
+
.object({
|
|
173
|
+
sessionUpdate: z.literal("current_mode_update"),
|
|
174
|
+
currentModeId: z.string().min(1),
|
|
175
|
+
_meta: MetaSchema,
|
|
176
|
+
})
|
|
177
|
+
.passthrough();
|
|
178
|
+
const UsageUpdate = z
|
|
179
|
+
.object({
|
|
180
|
+
sessionUpdate: z.literal("usage_update"),
|
|
181
|
+
size: z.number().int().nonnegative(),
|
|
182
|
+
used: z.number().int().nonnegative(),
|
|
183
|
+
cost: z.record(z.unknown()).nullish(),
|
|
184
|
+
_meta: MetaSchema,
|
|
185
|
+
})
|
|
186
|
+
.passthrough();
|
|
187
|
+
const KNOWN_UPDATE_SCHEMAS = {
|
|
188
|
+
user_message_chunk: MessageChunkUpdate("user_message_chunk"),
|
|
189
|
+
agent_message_chunk: MessageChunkUpdate("agent_message_chunk"),
|
|
190
|
+
agent_thought_chunk: MessageChunkUpdate("agent_thought_chunk"),
|
|
191
|
+
tool_call: ToolCallUpdate,
|
|
192
|
+
tool_call_update: ToolCallUpdateUpdate,
|
|
193
|
+
plan: PlanUpdate,
|
|
194
|
+
available_commands_update: AvailableCommandsUpdate,
|
|
195
|
+
current_mode_update: CurrentModeUpdate,
|
|
196
|
+
usage_update: UsageUpdate,
|
|
197
|
+
};
|
|
198
|
+
export const SessionUpdateSchema = z
|
|
199
|
+
.object({ sessionUpdate: z.string().min(1) })
|
|
200
|
+
.passthrough()
|
|
201
|
+
.superRefine((value, ctx) => {
|
|
202
|
+
const known = KNOWN_UPDATE_SCHEMAS[value.sessionUpdate];
|
|
203
|
+
if (!known) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const result = known.safeParse(value);
|
|
207
|
+
if (!result.success) {
|
|
208
|
+
for (const issue of result.error.issues) {
|
|
209
|
+
ctx.addIssue(issue);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
export const SessionUpdateNotificationSchema = z
|
|
214
|
+
.object({
|
|
215
|
+
sessionId: SessionIdSchema,
|
|
216
|
+
update: SessionUpdateSchema,
|
|
217
|
+
_meta: MetaSchema,
|
|
218
|
+
})
|
|
219
|
+
.passthrough();
|
|
220
|
+
export const KNOWN_SESSION_UPDATE_VARIANTS = [
|
|
221
|
+
"user_message_chunk",
|
|
222
|
+
"agent_message_chunk",
|
|
223
|
+
"agent_thought_chunk",
|
|
224
|
+
"tool_call",
|
|
225
|
+
"tool_call_update",
|
|
226
|
+
"plan",
|
|
227
|
+
"available_commands_update",
|
|
228
|
+
"current_mode_update",
|
|
229
|
+
"config_option_update",
|
|
230
|
+
"session_info_update",
|
|
231
|
+
"usage_update",
|
|
232
|
+
];
|
|
233
|
+
export function isUnknownSessionUpdate(update) {
|
|
234
|
+
return !KNOWN_SESSION_UPDATE_VARIANTS.includes(update.sessionUpdate);
|
|
235
|
+
}
|
|
236
|
+
export const PermissionOptionSchema = z
|
|
237
|
+
.object({
|
|
238
|
+
optionId: z.string().min(1),
|
|
239
|
+
name: z.string(),
|
|
240
|
+
kind: z.string().optional(),
|
|
241
|
+
_meta: MetaSchema,
|
|
242
|
+
})
|
|
243
|
+
.passthrough();
|
|
244
|
+
export const RequestPermissionRequestSchema = z
|
|
245
|
+
.object({
|
|
246
|
+
sessionId: SessionIdSchema,
|
|
247
|
+
options: z.array(PermissionOptionSchema).min(1),
|
|
248
|
+
toolCall: z.record(z.unknown()),
|
|
249
|
+
_meta: MetaSchema,
|
|
250
|
+
})
|
|
251
|
+
.passthrough();
|
|
252
|
+
export const RequestPermissionOutcomeSchema = z.union([
|
|
253
|
+
z.object({ outcome: z.literal("cancelled") }).passthrough(),
|
|
254
|
+
z.object({ outcome: z.literal("selected"), optionId: z.string().min(1) }).passthrough(),
|
|
255
|
+
]);
|
|
256
|
+
export const RequestPermissionResponseSchema = z
|
|
257
|
+
.object({
|
|
258
|
+
outcome: RequestPermissionOutcomeSchema,
|
|
259
|
+
_meta: MetaSchema,
|
|
260
|
+
})
|
|
261
|
+
.passthrough();
|
|
262
|
+
export const ReadTextFileRequestSchema = z
|
|
263
|
+
.object({
|
|
264
|
+
sessionId: SessionIdSchema,
|
|
265
|
+
path: z.string().min(1),
|
|
266
|
+
line: z.number().int().nonnegative().nullish(),
|
|
267
|
+
limit: z.number().int().nonnegative().nullish(),
|
|
268
|
+
_meta: MetaSchema,
|
|
269
|
+
})
|
|
270
|
+
.passthrough();
|
|
271
|
+
export const ReadTextFileResponseSchema = z
|
|
272
|
+
.object({
|
|
273
|
+
content: z.string(),
|
|
274
|
+
_meta: MetaSchema,
|
|
275
|
+
})
|
|
276
|
+
.passthrough();
|
|
277
|
+
export const WriteTextFileRequestSchema = z
|
|
278
|
+
.object({
|
|
279
|
+
sessionId: SessionIdSchema,
|
|
280
|
+
path: z.string().min(1),
|
|
281
|
+
content: z.string(),
|
|
282
|
+
_meta: MetaSchema,
|
|
283
|
+
})
|
|
284
|
+
.passthrough();
|
|
285
|
+
export const WriteTextFileResponseSchema = z.object({ _meta: MetaSchema }).passthrough();
|
|
286
|
+
export function parseAcp(schema, value, context) {
|
|
287
|
+
const result = schema.safeParse(value);
|
|
288
|
+
if (result.success) {
|
|
289
|
+
return result.data;
|
|
290
|
+
}
|
|
291
|
+
const issuePaths = result.error.issues.map(issue => ({
|
|
292
|
+
path: issue.path.join("."),
|
|
293
|
+
code: issue.code,
|
|
294
|
+
}));
|
|
295
|
+
throw new AcpProtocolError(`Malformed ACP ${context.method} payload`, {
|
|
296
|
+
provider: context.provider,
|
|
297
|
+
debug: { method: context.method, issues: issuePaths },
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
export function parseInitializeResponse(value, provider) {
|
|
301
|
+
return parseAcp(InitializeResponseSchema, value, { method: "initialize", provider });
|
|
302
|
+
}
|
|
303
|
+
export function parseSessionNewResponse(value, provider) {
|
|
304
|
+
return parseAcp(SessionNewResponseSchema, value, { method: "session/new", provider });
|
|
305
|
+
}
|
|
306
|
+
export function parseSessionLoadResponse(value, provider) {
|
|
307
|
+
return parseAcp(SessionLoadResponseSchema, value, { method: "session/load", provider });
|
|
308
|
+
}
|
|
309
|
+
export function parseSessionPromptResponse(value, provider) {
|
|
310
|
+
return parseAcp(SessionPromptResponseSchema, value, { method: "session/prompt", provider });
|
|
311
|
+
}
|
|
312
|
+
export function parseSessionUpdateNotification(value, provider) {
|
|
313
|
+
return parseAcp(SessionUpdateNotificationSchema, value, {
|
|
314
|
+
method: "session/update",
|
|
315
|
+
provider,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
export function parseRequestPermissionRequest(value, provider) {
|
|
319
|
+
return parseAcp(RequestPermissionRequestSchema, value, {
|
|
320
|
+
method: "session/request_permission",
|
|
321
|
+
provider,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
export function parseReadTextFileRequest(value, provider) {
|
|
325
|
+
return parseAcp(ReadTextFileRequestSchema, value, {
|
|
326
|
+
method: "fs/read_text_file",
|
|
327
|
+
provider,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
export function parseWriteTextFileRequest(value, provider) {
|
|
331
|
+
return parseAcp(WriteTextFileRequestSchema, value, {
|
|
332
|
+
method: "fs/write_text_file",
|
|
333
|
+
provider,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
@@ -67,6 +67,7 @@ export declare class AsyncJobManager {
|
|
|
67
67
|
private store;
|
|
68
68
|
private flightRecorder;
|
|
69
69
|
constructor(logger?: Logger, onJobComplete?: ((cli: LlmCli, durationMs: number, success: boolean) => void) | undefined, store?: JobStore | null, flightRecorder?: FlightRecorderLike);
|
|
70
|
+
private buildOrphanFlightResult;
|
|
70
71
|
checkStalledJobs(now?: number): void;
|
|
71
72
|
hasStore(): boolean;
|
|
72
73
|
private emitMetrics;
|
|
@@ -103,6 +104,7 @@ export declare class AsyncJobManager {
|
|
|
103
104
|
jobs: JobHealth[];
|
|
104
105
|
};
|
|
105
106
|
getJobOutputFormat(jobId: string): string | undefined;
|
|
107
|
+
getJobCli(jobId: string): LlmCli | undefined;
|
|
106
108
|
private snapshot;
|
|
107
109
|
private appendOutput;
|
|
108
110
|
}
|
|
@@ -3,7 +3,8 @@ import { envWithExtendedPath, getExtendedPath, killProcessGroup, providerCommand
|
|
|
3
3
|
import { noopLogger, logWarn } from "./logger.js";
|
|
4
4
|
import { ProcessMonitor } from "./process-monitor.js";
|
|
5
5
|
import { computeRequestKey } from "./job-store.js";
|
|
6
|
-
import { NoopFlightRecorder } from "./flight-recorder.js";
|
|
6
|
+
import { NoopFlightRecorder, } from "./flight-recorder.js";
|
|
7
|
+
import { codexFrResponse } from "./codex-json-parser.js";
|
|
7
8
|
const MAX_OUTPUT_SIZE = 50 * 1024 * 1024;
|
|
8
9
|
const JOB_TTL_MS = 60 * 60 * 1000;
|
|
9
10
|
const EVICTION_INTERVAL_MS = 5 * 60 * 1000;
|
|
@@ -75,17 +76,7 @@ export class AsyncJobManager {
|
|
|
75
76
|
}
|
|
76
77
|
for (const orphan of orphaned) {
|
|
77
78
|
try {
|
|
78
|
-
|
|
79
|
-
this.flightRecorder.logComplete(orphan.correlationId, {
|
|
80
|
-
response: orphan.stderr || orphan.stdout,
|
|
81
|
-
durationMs,
|
|
82
|
-
retryCount: 0,
|
|
83
|
-
circuitBreakerState: "closed",
|
|
84
|
-
optimizationApplied: false,
|
|
85
|
-
exitCode: orphan.exitCode ?? 1,
|
|
86
|
-
errorMessage: "orphaned after gateway restart",
|
|
87
|
-
status: "failed",
|
|
88
|
-
});
|
|
79
|
+
this.flightRecorder.logComplete(orphan.correlationId, this.buildOrphanFlightResult(orphan));
|
|
89
80
|
}
|
|
90
81
|
catch (err) {
|
|
91
82
|
this.logger.error(`Async-path FR logComplete for orphaned job ${orphan.id} failed`, err);
|
|
@@ -105,6 +96,33 @@ export class AsyncJobManager {
|
|
|
105
96
|
this.stallTimer.unref();
|
|
106
97
|
}
|
|
107
98
|
}
|
|
99
|
+
buildOrphanFlightResult(orphan) {
|
|
100
|
+
const durationMs = Math.max(0, Date.now() - new Date(orphan.startedAt).getTime());
|
|
101
|
+
const hasCapturedStdout = orphan.stdout.length > 0;
|
|
102
|
+
const hasKnownSuccessfulExit = orphan.exitCode === 0;
|
|
103
|
+
const hasCapturedResponseWithoutFailure = orphan.exitCode === null && hasCapturedStdout;
|
|
104
|
+
if (hasKnownSuccessfulExit || hasCapturedResponseWithoutFailure) {
|
|
105
|
+
return {
|
|
106
|
+
response: orphan.stdout,
|
|
107
|
+
durationMs,
|
|
108
|
+
retryCount: 0,
|
|
109
|
+
circuitBreakerState: "closed",
|
|
110
|
+
optimizationApplied: false,
|
|
111
|
+
exitCode: 0,
|
|
112
|
+
status: "completed",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
response: orphan.stderr || orphan.stdout,
|
|
117
|
+
durationMs,
|
|
118
|
+
retryCount: 0,
|
|
119
|
+
circuitBreakerState: "closed",
|
|
120
|
+
optimizationApplied: false,
|
|
121
|
+
exitCode: orphan.exitCode ?? 1,
|
|
122
|
+
errorMessage: "orphaned after gateway restart",
|
|
123
|
+
status: "failed",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
108
126
|
checkStalledJobs(now = Date.now()) {
|
|
109
127
|
for (const job of this.jobs.values()) {
|
|
110
128
|
if (job.status !== "running")
|
|
@@ -218,10 +236,11 @@ export class AsyncJobManager {
|
|
|
218
236
|
}
|
|
219
237
|
}
|
|
220
238
|
}
|
|
221
|
-
buildRequestKey(cli, args, env, stdin, cwd) {
|
|
239
|
+
buildRequestKey(cli, args, env, stdin, cwd, outputFormat) {
|
|
222
240
|
const extraEnv = canonicaliseEnvForKey(env);
|
|
223
241
|
const withStdin = stdin === undefined ? extraEnv : `${extraEnv}|stdin:${stdin}`;
|
|
224
|
-
const
|
|
242
|
+
const withCwd = cwd === undefined ? withStdin : `${withStdin}|cwd:${cwd}`;
|
|
243
|
+
const extra = cli === "codex" ? `${withCwd}|fmt:${outputFormat ?? "text"}` : withCwd;
|
|
225
244
|
return computeRequestKey(cli, args, extra);
|
|
226
245
|
}
|
|
227
246
|
fireOnComplete(job) {
|
|
@@ -247,7 +266,14 @@ export class AsyncJobManager {
|
|
|
247
266
|
const durationMs = Math.max(0, Date.now() - new Date(job.startedAt).getTime());
|
|
248
267
|
const usage = finalStatus === "completed" && job.extractUsage ? this.safeExtractUsage(job) : {};
|
|
249
268
|
const isFailure = finalStatus === "failed";
|
|
250
|
-
|
|
269
|
+
let response;
|
|
270
|
+
if (job.cli === "codex") {
|
|
271
|
+
const codexText = codexFrResponse(job.outputFormat, job.stdout);
|
|
272
|
+
response = isFailure ? job.stderr || codexText : codexText;
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
response = isFailure ? job.stderr || job.stdout : job.stdout;
|
|
276
|
+
}
|
|
251
277
|
const exitCode = job.exitCode ?? (finalStatus === "completed" ? 0 : 1);
|
|
252
278
|
const errorMessage = isFailure
|
|
253
279
|
? (overrideErrorMessage ?? job.error ?? job.stderr ?? `Exit code ${exitCode}`)
|
|
@@ -402,7 +428,7 @@ export class AsyncJobManager {
|
|
|
402
428
|
}
|
|
403
429
|
startJobWithDedup(cli, args, correlationId, opts = {}) {
|
|
404
430
|
const { cwd, idleTimeoutMs, outputFormat, forceRefresh, env: extraEnv, stdin, onComplete, flightRecorderEntry, extractUsage, writeFlightStart, } = opts;
|
|
405
|
-
const requestKey = this.buildRequestKey(cli, args, extraEnv, stdin, cwd);
|
|
431
|
+
const requestKey = this.buildRequestKey(cli, args, extraEnv, stdin, cwd, outputFormat);
|
|
406
432
|
if (!forceRefresh && this.store) {
|
|
407
433
|
try {
|
|
408
434
|
const existing = this.store.findByRequestKey(requestKey);
|
|
@@ -713,6 +739,9 @@ export class AsyncJobManager {
|
|
|
713
739
|
getJobOutputFormat(jobId) {
|
|
714
740
|
return this.jobs.get(jobId)?.outputFormat;
|
|
715
741
|
}
|
|
742
|
+
getJobCli(jobId) {
|
|
743
|
+
return this.jobs.get(jobId)?.cli;
|
|
744
|
+
}
|
|
716
745
|
snapshot(job) {
|
|
717
746
|
return {
|
|
718
747
|
id: job.id,
|
package/dist/cache-stats.js
CHANGED
|
@@ -5,6 +5,11 @@ function safeNum(n) {
|
|
|
5
5
|
function isCacheStatsCli(s) {
|
|
6
6
|
return s === "claude" || s === "codex" || s === "gemini" || s === "grok" || s === "mistral";
|
|
7
7
|
}
|
|
8
|
+
function normalizeCacheStatsCli(s) {
|
|
9
|
+
if (s === "grok-api")
|
|
10
|
+
return "grok";
|
|
11
|
+
return isCacheStatsCli(s) ? s : null;
|
|
12
|
+
}
|
|
8
13
|
export function computeSessionCacheStats(db, sessionId) {
|
|
9
14
|
const rows = db.queryRequests(`SELECT cli, model,
|
|
10
15
|
COALESCE(cache_read_tokens, 0) AS cache_read_tokens,
|
|
@@ -34,10 +39,11 @@ export function computeSessionCacheStats(db, sessionId) {
|
|
|
34
39
|
prefixSet.add(row.stable_prefix_hash);
|
|
35
40
|
if (!lastAt || row.datetime_utc > lastAt)
|
|
36
41
|
lastAt = row.datetime_utc;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
const rowCli = normalizeCacheStatsCli(row.cli);
|
|
43
|
+
if (cli === null && rowCli !== null)
|
|
44
|
+
cli = rowCli;
|
|
45
|
+
if (rowCli !== null) {
|
|
46
|
+
estimatedSavingsUsd += estimateCacheSavingsUsd(rowCli, row.model, reads);
|
|
41
47
|
}
|
|
42
48
|
}
|
|
43
49
|
const requestCount = rows.length;
|
|
@@ -102,15 +108,16 @@ export function computePrefixCacheStats(db, stablePrefixHash) {
|
|
|
102
108
|
if (!firstAt)
|
|
103
109
|
firstAt = row.datetime_utc;
|
|
104
110
|
lastAt = row.datetime_utc;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
const rowCli = normalizeCacheStatsCli(row.cli);
|
|
112
|
+
if (rowCli !== null) {
|
|
113
|
+
estimatedSavingsUsd += estimateCacheSavingsUsd(rowCli, row.model, reads);
|
|
114
|
+
const key = `${rowCli}::${row.model}`;
|
|
108
115
|
const entry = cliMap.get(key);
|
|
109
116
|
if (entry) {
|
|
110
117
|
entry.count += 1;
|
|
111
118
|
}
|
|
112
119
|
else {
|
|
113
|
-
cliMap.set(key, { cli:
|
|
120
|
+
cliMap.set(key, { cli: rowCli, model: row.model, count: 1 });
|
|
114
121
|
}
|
|
115
122
|
}
|
|
116
123
|
}
|
|
@@ -180,9 +187,9 @@ export function computeGlobalCacheStats(db, opts = {}) {
|
|
|
180
187
|
arr.push({ datetime_utc: row.datetime_utc, cache_creation_tokens: creation });
|
|
181
188
|
perPrefix.set(row.stable_prefix_hash, arr);
|
|
182
189
|
}
|
|
183
|
-
|
|
190
|
+
const cli = normalizeCacheStatsCli(row.cli);
|
|
191
|
+
if (cli === null)
|
|
184
192
|
continue;
|
|
185
|
-
const cli = row.cli;
|
|
186
193
|
const savings = estimateCacheSavingsUsd(cli, row.model, reads);
|
|
187
194
|
totalSavings += savings;
|
|
188
195
|
const agg = perCliMap.get(cli) ?? {
|
|
@@ -10,5 +10,8 @@ export interface CodexJsonParseResult {
|
|
|
10
10
|
error?: string;
|
|
11
11
|
threadId?: string;
|
|
12
12
|
finalMessage?: string;
|
|
13
|
+
sawEvent?: boolean;
|
|
13
14
|
}
|
|
14
15
|
export declare function parseCodexJsonStream(stdout: string): CodexJsonParseResult;
|
|
16
|
+
export declare function codexDisplayText(stdout: string): string;
|
|
17
|
+
export declare function codexFrResponse(outputFormat: string | undefined, stdout: string): string;
|
|
@@ -13,6 +13,7 @@ export function parseCodexJsonStream(stdout) {
|
|
|
13
13
|
if (!parsed || typeof parsed !== "object") {
|
|
14
14
|
continue;
|
|
15
15
|
}
|
|
16
|
+
result.sawEvent = true;
|
|
16
17
|
switch (parsed.type) {
|
|
17
18
|
case "thread.started":
|
|
18
19
|
if (typeof parsed.thread_id === "string") {
|
|
@@ -85,3 +86,19 @@ export function parseCodexJsonStream(stdout) {
|
|
|
85
86
|
}
|
|
86
87
|
return result;
|
|
87
88
|
}
|
|
89
|
+
export function codexDisplayText(stdout) {
|
|
90
|
+
const parsed = parseCodexJsonStream(stdout);
|
|
91
|
+
if (parsed.finalMessage !== undefined) {
|
|
92
|
+
return parsed.finalMessage;
|
|
93
|
+
}
|
|
94
|
+
if (parsed.error !== undefined) {
|
|
95
|
+
return parsed.error;
|
|
96
|
+
}
|
|
97
|
+
if (parsed.sawEvent) {
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
return stdout;
|
|
101
|
+
}
|
|
102
|
+
export function codexFrResponse(outputFormat, stdout) {
|
|
103
|
+
return outputFormat === "json" ? stdout : codexDisplayText(stdout);
|
|
104
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -76,4 +76,34 @@ export interface ProvidersConfig {
|
|
|
76
76
|
}
|
|
77
77
|
export declare function loadProvidersConfig(logger?: Logger): ProvidersConfig;
|
|
78
78
|
export declare function isXaiProviderEnabled(config: ProvidersConfig, env?: NodeJS.ProcessEnv): boolean;
|
|
79
|
+
export declare const ACP_TRANSPORTS: readonly ["cli", "acp"];
|
|
80
|
+
export type AcpTransport = (typeof ACP_TRANSPORTS)[number];
|
|
81
|
+
export declare const DEFAULT_ACP_PROCESS_IDLE_TIMEOUT_MS = 600000;
|
|
82
|
+
export declare const DEFAULT_ACP_INITIALIZE_TIMEOUT_MS = 10000;
|
|
83
|
+
export declare const DEFAULT_ACP_SESSION_NEW_TIMEOUT_MS = 10000;
|
|
84
|
+
export declare const DEFAULT_ACP_PROMPT_TIMEOUT_MS = 600000;
|
|
85
|
+
export interface AcpProviderConfig {
|
|
86
|
+
enabled: boolean;
|
|
87
|
+
command: string;
|
|
88
|
+
args: string[];
|
|
89
|
+
runtimeEnabled: boolean;
|
|
90
|
+
isolatedLeaderSocket: boolean;
|
|
91
|
+
}
|
|
92
|
+
export interface AcpConfig {
|
|
93
|
+
enabled: boolean;
|
|
94
|
+
defaultTransport: AcpTransport;
|
|
95
|
+
smokeOnStartup: boolean;
|
|
96
|
+
processIdleTimeoutMs: number;
|
|
97
|
+
initializeTimeoutMs: number;
|
|
98
|
+
sessionNewTimeoutMs: number;
|
|
99
|
+
promptTimeoutMs: number;
|
|
100
|
+
allowWriteHostServices: boolean;
|
|
101
|
+
allowTerminalHostServices: boolean;
|
|
102
|
+
fallbackToCliWhenUnhealthy: boolean;
|
|
103
|
+
providers: Record<string, AcpProviderConfig>;
|
|
104
|
+
sources: {
|
|
105
|
+
configFile: string | null;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export declare function loadAcpConfig(logger?: Logger): AcpConfig;
|
|
79
109
|
export declare function loadRemoteOAuthConfig(logger?: Logger, env?: NodeJS.ProcessEnv): RemoteOAuthConfig;
|