@gakim-digital/dexter-bridge 0.5.14 → 0.5.17
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
CHANGED
|
@@ -286,7 +286,7 @@ export function createClaudeAgentSdkAdapter({
|
|
|
286
286
|
return {
|
|
287
287
|
...inherited,
|
|
288
288
|
CLAUDE_CONFIG_DIR: configDir,
|
|
289
|
-
CLAUDE_AGENT_SDK_CLIENT_APP: 'dexter-hosted-worker/0.2.
|
|
289
|
+
CLAUDE_AGENT_SDK_CLIENT_APP: 'dexter-hosted-worker/0.2.4',
|
|
290
290
|
};
|
|
291
291
|
}
|
|
292
292
|
|
|
@@ -2,6 +2,11 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createJsonRpcClient } from './jsonRpcClient.js';
|
|
5
|
+
import {
|
|
6
|
+
codexTransportOutputSchema,
|
|
7
|
+
codexTransportPrompt,
|
|
8
|
+
decodeCodexTransportOutput,
|
|
9
|
+
} from './codexStructuredOutput.js';
|
|
5
10
|
import { normalizeCompanionTokenUsage } from '../agentOutput.js';
|
|
6
11
|
import { BRIDGE_VERSION } from '../config.js';
|
|
7
12
|
|
|
@@ -13,12 +18,13 @@ import { BRIDGE_VERSION } from '../config.js';
|
|
|
13
18
|
* That gives us persistent threads, device-code login, and real plan/rate-limit
|
|
14
19
|
* data straight from Codex.
|
|
15
20
|
*
|
|
16
|
-
* Protocol verified against codex-cli 0.
|
|
21
|
+
* Protocol verified against codex-cli 0.146.0 (`codex app-server
|
|
17
22
|
* generate-json-schema`): `initialize`, `model/list`, `account/read`,
|
|
18
23
|
* `account/login/start` (with `{type:'chatgptDeviceCode'}` →
|
|
19
24
|
* `{loginId,userCode,verificationUrl}`), `thread/start`, `turn/start`,
|
|
20
25
|
* `turn/interrupt`, `account/logout`; server pushes `item/completed`,
|
|
21
|
-
* `
|
|
26
|
+
* `thread/tokenUsage/updated`, `turn/completed`,
|
|
27
|
+
* `account/login/completed`, `error`.
|
|
22
28
|
*/
|
|
23
29
|
|
|
24
30
|
const CLIENT_INFO = {
|
|
@@ -77,9 +83,6 @@ function codexModelOnlyConfig() {
|
|
|
77
83
|
shell_tool: false,
|
|
78
84
|
skill_mcp_dependency_install: false,
|
|
79
85
|
},
|
|
80
|
-
agents: {
|
|
81
|
-
enabled: false,
|
|
82
|
-
},
|
|
83
86
|
memories: {
|
|
84
87
|
generate_memories: false,
|
|
85
88
|
use_memories: false,
|
|
@@ -132,6 +135,7 @@ export const CODEX_APP_SERVER_METHODS = {
|
|
|
132
135
|
export const CODEX_APP_SERVER_NOTIFICATIONS = {
|
|
133
136
|
loginCompleted: 'account/login/completed',
|
|
134
137
|
itemCompleted: 'item/completed',
|
|
138
|
+
tokenUsageUpdated: 'thread/tokenUsage/updated',
|
|
135
139
|
turnCompleted: 'turn/completed',
|
|
136
140
|
threadStarted: 'thread/started',
|
|
137
141
|
error: 'error',
|
|
@@ -166,9 +170,36 @@ export function normalizeCodexModels(response) {
|
|
|
166
170
|
}));
|
|
167
171
|
}
|
|
168
172
|
|
|
169
|
-
export function
|
|
170
|
-
const
|
|
171
|
-
const
|
|
173
|
+
export function codexUsageSince(current, baseline = {}) {
|
|
174
|
+
const currentUsage = normalizeCompanionTokenUsage(current);
|
|
175
|
+
const baselineUsage = normalizeCompanionTokenUsage(baseline);
|
|
176
|
+
const inputTokens = Math.max(0, currentUsage.inputTokens - baselineUsage.inputTokens);
|
|
177
|
+
const outputTokens = Math.max(0, currentUsage.outputTokens - baselineUsage.outputTokens);
|
|
178
|
+
const cachedInputTokens = Math.max(
|
|
179
|
+
0,
|
|
180
|
+
currentUsage.cachedInputTokens - baselineUsage.cachedInputTokens,
|
|
181
|
+
);
|
|
182
|
+
const cacheWriteInputTokens = Math.max(
|
|
183
|
+
0,
|
|
184
|
+
currentUsage.cacheWriteInputTokens - baselineUsage.cacheWriteInputTokens,
|
|
185
|
+
);
|
|
186
|
+
const reasoningOutputTokens = Math.max(
|
|
187
|
+
0,
|
|
188
|
+
currentUsage.reasoningOutputTokens - baselineUsage.reasoningOutputTokens,
|
|
189
|
+
);
|
|
190
|
+
const totalTokens = Math.max(
|
|
191
|
+
0,
|
|
192
|
+
currentUsage.totalTokens - baselineUsage.totalTokens,
|
|
193
|
+
inputTokens + outputTokens + reasoningOutputTokens,
|
|
194
|
+
);
|
|
195
|
+
const tokenUsage = {
|
|
196
|
+
inputTokens,
|
|
197
|
+
outputTokens,
|
|
198
|
+
cachedInputTokens,
|
|
199
|
+
cacheWriteInputTokens,
|
|
200
|
+
reasoningOutputTokens,
|
|
201
|
+
totalTokens,
|
|
202
|
+
};
|
|
172
203
|
return {
|
|
173
204
|
tokenUsage,
|
|
174
205
|
usageAvailable: tokenUsage.totalTokens > 0,
|
|
@@ -217,6 +248,8 @@ export function createCodexAppServerAdapter({
|
|
|
217
248
|
);
|
|
218
249
|
/** threadId per Dexter turn, so relay runs in one turn share Codex context. */
|
|
219
250
|
const threadsByRun = new Map();
|
|
251
|
+
/** Latest cumulative token totals reported for each persistent Codex thread. */
|
|
252
|
+
const threadUsageTotals = new Map();
|
|
220
253
|
const listeners = new Set();
|
|
221
254
|
|
|
222
255
|
function emit(event) {
|
|
@@ -238,6 +271,15 @@ export function createCodexAppServerAdapter({
|
|
|
238
271
|
cwd,
|
|
239
272
|
onNotification: (message) => {
|
|
240
273
|
trace?.info('codex_app_server_notification', { method: message.method });
|
|
274
|
+
if (
|
|
275
|
+
message.method === CODEX_APP_SERVER_NOTIFICATIONS.tokenUsageUpdated
|
|
276
|
+
&& typeof message.params?.threadId === 'string'
|
|
277
|
+
) {
|
|
278
|
+
threadUsageTotals.set(
|
|
279
|
+
message.params.threadId,
|
|
280
|
+
normalizeCompanionTokenUsage(message.params?.tokenUsage?.total),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
241
283
|
emit({ type: 'notification', method: message.method, params: message.params });
|
|
242
284
|
},
|
|
243
285
|
// The app server asks for approvals; Dexter runs read-only against Codex,
|
|
@@ -256,6 +298,7 @@ export function createCodexAppServerAdapter({
|
|
|
256
298
|
onExit: (info) => {
|
|
257
299
|
initialized = null;
|
|
258
300
|
threadsByRun.clear();
|
|
301
|
+
threadUsageTotals.clear();
|
|
259
302
|
trace?.info('codex_app_server_exit', info || {});
|
|
260
303
|
emit({ type: 'exit', ...info });
|
|
261
304
|
},
|
|
@@ -406,6 +449,7 @@ export function createCodexAppServerAdapter({
|
|
|
406
449
|
if (oldest) {
|
|
407
450
|
const [oldestRunId, oldestThreadId] = oldest;
|
|
408
451
|
threadsByRun.delete(oldestRunId);
|
|
452
|
+
threadUsageTotals.delete(oldestThreadId);
|
|
409
453
|
active.request(
|
|
410
454
|
CODEX_APP_SERVER_METHODS.threadUnsubscribe,
|
|
411
455
|
{ threadId: oldestThreadId },
|
|
@@ -433,6 +477,7 @@ export function createCodexAppServerAdapter({
|
|
|
433
477
|
const threadId = response?.thread?.id || response?.threadId || response?.id;
|
|
434
478
|
if (!threadId) throw new Error('Codex app server did not return a thread id.');
|
|
435
479
|
threadsByRun.set(runId, threadId);
|
|
480
|
+
threadUsageTotals.delete(threadId);
|
|
436
481
|
return threadId;
|
|
437
482
|
}
|
|
438
483
|
|
|
@@ -454,9 +499,20 @@ export function createCodexAppServerAdapter({
|
|
|
454
499
|
const threadKey = sessionId || runId;
|
|
455
500
|
if (!threadKey) throw new Error('Codex model turn requires a session id.');
|
|
456
501
|
const threadId = await ensureThread(threadKey, { model });
|
|
502
|
+
const baselineUsage = threadUsageTotals.get(threadId) || normalizeCompanionTokenUsage();
|
|
503
|
+
const transportSchema = outputSchema
|
|
504
|
+
? codexTransportOutputSchema(outputSchema)
|
|
505
|
+
: null;
|
|
506
|
+
const transportPrompt = transportSchema
|
|
507
|
+
? codexTransportPrompt(prompt)
|
|
508
|
+
: prompt;
|
|
457
509
|
|
|
458
510
|
return new Promise((resolve, reject) => {
|
|
459
511
|
let lastMessage = '';
|
|
512
|
+
const usageForTurn = () => codexUsageSince(
|
|
513
|
+
threadUsageTotals.get(threadId),
|
|
514
|
+
baselineUsage,
|
|
515
|
+
);
|
|
460
516
|
const timer = setTimeout(() => {
|
|
461
517
|
listeners.delete(listener);
|
|
462
518
|
active.request(
|
|
@@ -464,7 +520,9 @@ export function createCodexAppServerAdapter({
|
|
|
464
520
|
{ threadId },
|
|
465
521
|
{ timeoutMs: 10000 },
|
|
466
522
|
).catch(() => undefined);
|
|
467
|
-
|
|
523
|
+
const error = new Error(`Codex turn timed out after ${timeoutMs}ms.`);
|
|
524
|
+
error.companionUsage = usageForTurn();
|
|
525
|
+
reject(error);
|
|
468
526
|
}, timeoutMs);
|
|
469
527
|
|
|
470
528
|
function finish(fn, value) {
|
|
@@ -491,7 +549,9 @@ export function createCodexAppServerAdapter({
|
|
|
491
549
|
// final turn outcome. Let its internal retry finish; the terminal
|
|
492
550
|
// turn/completed event remains authoritative.
|
|
493
551
|
if (event.params?.willRetry === true) return;
|
|
494
|
-
|
|
552
|
+
const error = new Error(codexTurnErrorMessage(event.params));
|
|
553
|
+
error.companionUsage = usageForTurn();
|
|
554
|
+
finish(reject, error);
|
|
495
555
|
return;
|
|
496
556
|
}
|
|
497
557
|
if (event.method === CODEX_APP_SERVER_NOTIFICATIONS.turnCompleted) {
|
|
@@ -501,15 +561,24 @@ export function createCodexAppServerAdapter({
|
|
|
501
561
|
turn,
|
|
502
562
|
'Codex turn failed without an error message.',
|
|
503
563
|
));
|
|
504
|
-
error.companionUsage =
|
|
564
|
+
error.companionUsage = usageForTurn();
|
|
505
565
|
finish(reject, error);
|
|
506
566
|
return;
|
|
507
567
|
}
|
|
568
|
+
let decodedMessage = lastMessage;
|
|
569
|
+
if (transportSchema) {
|
|
570
|
+
try {
|
|
571
|
+
decodedMessage = decodeCodexTransportOutput(lastMessage);
|
|
572
|
+
} catch (error) {
|
|
573
|
+
finish(reject, error);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
508
577
|
finish(resolve, {
|
|
509
|
-
text:
|
|
578
|
+
text: decodedMessage,
|
|
510
579
|
threadId,
|
|
511
580
|
turnId: turn.id || null,
|
|
512
|
-
...
|
|
581
|
+
...usageForTurn(),
|
|
513
582
|
});
|
|
514
583
|
}
|
|
515
584
|
}
|
|
@@ -521,9 +590,9 @@ export function createCodexAppServerAdapter({
|
|
|
521
590
|
CODEX_APP_SERVER_METHODS.turnStart,
|
|
522
591
|
{
|
|
523
592
|
threadId,
|
|
524
|
-
input: [{ type: 'text', text:
|
|
593
|
+
input: [{ type: 'text', text: transportPrompt }],
|
|
525
594
|
...(model ? { model } : {}),
|
|
526
|
-
...(
|
|
595
|
+
...(transportSchema ? { outputSchema: transportSchema } : {}),
|
|
527
596
|
},
|
|
528
597
|
{ timeoutMs: 30000 },
|
|
529
598
|
)
|
|
@@ -545,6 +614,7 @@ export function createCodexAppServerAdapter({
|
|
|
545
614
|
const threadId = threadsByRun.get(sessionId);
|
|
546
615
|
if (!threadId) return;
|
|
547
616
|
threadsByRun.delete(sessionId);
|
|
617
|
+
threadUsageTotals.delete(threadId);
|
|
548
618
|
if (!client || client.closed) return;
|
|
549
619
|
await client.request(
|
|
550
620
|
CODEX_APP_SERVER_METHODS.threadUnsubscribe,
|
|
@@ -559,11 +629,13 @@ export function createCodexAppServerAdapter({
|
|
|
559
629
|
await client.request(CODEX_APP_SERVER_METHODS.logout, {}, { timeoutMs: 15000 });
|
|
560
630
|
} finally {
|
|
561
631
|
threadsByRun.clear();
|
|
632
|
+
threadUsageTotals.clear();
|
|
562
633
|
}
|
|
563
634
|
}
|
|
564
635
|
|
|
565
636
|
function close() {
|
|
566
637
|
threadsByRun.clear();
|
|
638
|
+
threadUsageTotals.clear();
|
|
567
639
|
listeners.clear();
|
|
568
640
|
client?.close();
|
|
569
641
|
client = null;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function finiteInteger(value) {
|
|
6
|
+
const parsed = Number(value);
|
|
7
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function toolCallVariants(outputSchema) {
|
|
11
|
+
const items = outputSchema?.properties?.toolCalls?.items;
|
|
12
|
+
if (!isRecord(items)) return [];
|
|
13
|
+
return Array.isArray(items.anyOf)
|
|
14
|
+
? items.anyOf.filter(isRecord)
|
|
15
|
+
: [items];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function toolNames(outputSchema) {
|
|
19
|
+
return Array.from(new Set(
|
|
20
|
+
toolCallVariants(outputSchema).flatMap((variant) => {
|
|
21
|
+
const names = variant?.properties?.name?.enum;
|
|
22
|
+
return Array.isArray(names)
|
|
23
|
+
? names.filter((name) => typeof name === 'string' && name)
|
|
24
|
+
: [];
|
|
25
|
+
}),
|
|
26
|
+
));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Codex App Server sends outputSchema through OpenAI Structured Outputs, whose
|
|
31
|
+
* supported JSON Schema subset requires closed objects with every property in
|
|
32
|
+
* `required`. Dexter's canonical tool schemas intentionally contain optional
|
|
33
|
+
* and open-ended fields, so they must not be embedded directly in that schema.
|
|
34
|
+
*
|
|
35
|
+
* Keep the strict response format as a transport envelope. The full canonical
|
|
36
|
+
* tool catalog remains in the prompt and the server validates decoded arguments
|
|
37
|
+
* before execution.
|
|
38
|
+
*/
|
|
39
|
+
export function codexTransportOutputSchema(outputSchema) {
|
|
40
|
+
if (!isRecord(outputSchema)) {
|
|
41
|
+
throw new Error('Codex model turn requires an output schema.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const sourceToolCalls = outputSchema?.properties?.toolCalls;
|
|
45
|
+
const sourceFinishReason = outputSchema?.properties?.finishReason;
|
|
46
|
+
if (!isRecord(sourceToolCalls) || !isRecord(sourceFinishReason)) {
|
|
47
|
+
throw new Error('Codex model turn output schema is missing its response envelope.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const names = toolNames(outputSchema);
|
|
51
|
+
const minItems = finiteInteger(sourceToolCalls.minItems);
|
|
52
|
+
const maxItems = finiteInteger(sourceToolCalls.maxItems);
|
|
53
|
+
const finishReasonEnum = Array.isArray(sourceFinishReason.enum)
|
|
54
|
+
? sourceFinishReason.enum.filter((value) => typeof value === 'string')
|
|
55
|
+
: ['tool_calls', 'stop', 'length'];
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
text: { type: 'string' },
|
|
61
|
+
toolCalls: {
|
|
62
|
+
type: 'array',
|
|
63
|
+
...(minItems === undefined ? {} : { minItems }),
|
|
64
|
+
...(maxItems === undefined ? {} : { maxItems }),
|
|
65
|
+
items: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
id: { type: 'string' },
|
|
69
|
+
name: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
...(names.length ? { enum: names } : {}),
|
|
72
|
+
},
|
|
73
|
+
arguments: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
description:
|
|
76
|
+
'A JSON-encoded object matching the selected Dexter tool schema.',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
required: ['id', 'name', 'arguments'],
|
|
80
|
+
additionalProperties: false,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
finishReason: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
enum: finishReasonEnum.length
|
|
86
|
+
? finishReasonEnum
|
|
87
|
+
: ['tool_calls', 'stop', 'length'],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
required: ['text', 'toolCalls', 'finishReason'],
|
|
91
|
+
additionalProperties: false,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function codexTransportPrompt(prompt) {
|
|
96
|
+
return [
|
|
97
|
+
String(prompt || ''),
|
|
98
|
+
'',
|
|
99
|
+
'Codex structured-output transport:',
|
|
100
|
+
'In the final JSON response, encode each toolCalls[].arguments value as a JSON string.',
|
|
101
|
+
'The decoded string must be one JSON object matching the exact selected tool schema in the Tools catalog.',
|
|
102
|
+
'Do not omit the id, name, arguments, text, toolCalls, or finishReason fields.',
|
|
103
|
+
].join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function decodeCodexTransportOutput(text) {
|
|
107
|
+
let parsed;
|
|
108
|
+
try {
|
|
109
|
+
parsed = JSON.parse(String(text || '').trim());
|
|
110
|
+
} catch {
|
|
111
|
+
throw new Error('Codex returned malformed structured output.');
|
|
112
|
+
}
|
|
113
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.toolCalls)) {
|
|
114
|
+
throw new Error('Codex returned an invalid model-turn response envelope.');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const toolCalls = parsed.toolCalls.map((call, index) => {
|
|
118
|
+
if (!isRecord(call) || typeof call.arguments !== 'string') {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Codex tool call ${index + 1} did not use the JSON-string arguments transport.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let args;
|
|
125
|
+
try {
|
|
126
|
+
args = JSON.parse(call.arguments);
|
|
127
|
+
} catch {
|
|
128
|
+
throw new Error(`Codex tool call ${index + 1} contained malformed JSON arguments.`);
|
|
129
|
+
}
|
|
130
|
+
if (!isRecord(args)) {
|
|
131
|
+
throw new Error(`Codex tool call ${index + 1} arguments must decode to an object.`);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
...call,
|
|
135
|
+
arguments: args,
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return JSON.stringify({
|
|
140
|
+
...parsed,
|
|
141
|
+
toolCalls,
|
|
142
|
+
});
|
|
143
|
+
}
|