@gakim-digital/dexter-bridge 0.5.15 → 0.5.19
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 = {
|
|
@@ -129,6 +135,7 @@ export const CODEX_APP_SERVER_METHODS = {
|
|
|
129
135
|
export const CODEX_APP_SERVER_NOTIFICATIONS = {
|
|
130
136
|
loginCompleted: 'account/login/completed',
|
|
131
137
|
itemCompleted: 'item/completed',
|
|
138
|
+
tokenUsageUpdated: 'thread/tokenUsage/updated',
|
|
132
139
|
turnCompleted: 'turn/completed',
|
|
133
140
|
threadStarted: 'thread/started',
|
|
134
141
|
error: 'error',
|
|
@@ -163,9 +170,36 @@ export function normalizeCodexModels(response) {
|
|
|
163
170
|
}));
|
|
164
171
|
}
|
|
165
172
|
|
|
166
|
-
export function
|
|
167
|
-
const
|
|
168
|
-
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
|
+
};
|
|
169
203
|
return {
|
|
170
204
|
tokenUsage,
|
|
171
205
|
usageAvailable: tokenUsage.totalTokens > 0,
|
|
@@ -214,6 +248,8 @@ export function createCodexAppServerAdapter({
|
|
|
214
248
|
);
|
|
215
249
|
/** threadId per Dexter turn, so relay runs in one turn share Codex context. */
|
|
216
250
|
const threadsByRun = new Map();
|
|
251
|
+
/** Latest cumulative token totals reported for each persistent Codex thread. */
|
|
252
|
+
const threadUsageTotals = new Map();
|
|
217
253
|
const listeners = new Set();
|
|
218
254
|
|
|
219
255
|
function emit(event) {
|
|
@@ -235,6 +271,15 @@ export function createCodexAppServerAdapter({
|
|
|
235
271
|
cwd,
|
|
236
272
|
onNotification: (message) => {
|
|
237
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
|
+
}
|
|
238
283
|
emit({ type: 'notification', method: message.method, params: message.params });
|
|
239
284
|
},
|
|
240
285
|
// The app server asks for approvals; Dexter runs read-only against Codex,
|
|
@@ -253,6 +298,7 @@ export function createCodexAppServerAdapter({
|
|
|
253
298
|
onExit: (info) => {
|
|
254
299
|
initialized = null;
|
|
255
300
|
threadsByRun.clear();
|
|
301
|
+
threadUsageTotals.clear();
|
|
256
302
|
trace?.info('codex_app_server_exit', info || {});
|
|
257
303
|
emit({ type: 'exit', ...info });
|
|
258
304
|
},
|
|
@@ -403,6 +449,7 @@ export function createCodexAppServerAdapter({
|
|
|
403
449
|
if (oldest) {
|
|
404
450
|
const [oldestRunId, oldestThreadId] = oldest;
|
|
405
451
|
threadsByRun.delete(oldestRunId);
|
|
452
|
+
threadUsageTotals.delete(oldestThreadId);
|
|
406
453
|
active.request(
|
|
407
454
|
CODEX_APP_SERVER_METHODS.threadUnsubscribe,
|
|
408
455
|
{ threadId: oldestThreadId },
|
|
@@ -430,6 +477,7 @@ export function createCodexAppServerAdapter({
|
|
|
430
477
|
const threadId = response?.thread?.id || response?.threadId || response?.id;
|
|
431
478
|
if (!threadId) throw new Error('Codex app server did not return a thread id.');
|
|
432
479
|
threadsByRun.set(runId, threadId);
|
|
480
|
+
threadUsageTotals.delete(threadId);
|
|
433
481
|
return threadId;
|
|
434
482
|
}
|
|
435
483
|
|
|
@@ -451,9 +499,20 @@ export function createCodexAppServerAdapter({
|
|
|
451
499
|
const threadKey = sessionId || runId;
|
|
452
500
|
if (!threadKey) throw new Error('Codex model turn requires a session id.');
|
|
453
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;
|
|
454
509
|
|
|
455
510
|
return new Promise((resolve, reject) => {
|
|
456
511
|
let lastMessage = '';
|
|
512
|
+
const usageForTurn = () => codexUsageSince(
|
|
513
|
+
threadUsageTotals.get(threadId),
|
|
514
|
+
baselineUsage,
|
|
515
|
+
);
|
|
457
516
|
const timer = setTimeout(() => {
|
|
458
517
|
listeners.delete(listener);
|
|
459
518
|
active.request(
|
|
@@ -461,7 +520,9 @@ export function createCodexAppServerAdapter({
|
|
|
461
520
|
{ threadId },
|
|
462
521
|
{ timeoutMs: 10000 },
|
|
463
522
|
).catch(() => undefined);
|
|
464
|
-
|
|
523
|
+
const error = new Error(`Codex turn timed out after ${timeoutMs}ms.`);
|
|
524
|
+
error.companionUsage = usageForTurn();
|
|
525
|
+
reject(error);
|
|
465
526
|
}, timeoutMs);
|
|
466
527
|
|
|
467
528
|
function finish(fn, value) {
|
|
@@ -488,7 +549,9 @@ export function createCodexAppServerAdapter({
|
|
|
488
549
|
// final turn outcome. Let its internal retry finish; the terminal
|
|
489
550
|
// turn/completed event remains authoritative.
|
|
490
551
|
if (event.params?.willRetry === true) return;
|
|
491
|
-
|
|
552
|
+
const error = new Error(codexTurnErrorMessage(event.params));
|
|
553
|
+
error.companionUsage = usageForTurn();
|
|
554
|
+
finish(reject, error);
|
|
492
555
|
return;
|
|
493
556
|
}
|
|
494
557
|
if (event.method === CODEX_APP_SERVER_NOTIFICATIONS.turnCompleted) {
|
|
@@ -498,15 +561,24 @@ export function createCodexAppServerAdapter({
|
|
|
498
561
|
turn,
|
|
499
562
|
'Codex turn failed without an error message.',
|
|
500
563
|
));
|
|
501
|
-
error.companionUsage =
|
|
564
|
+
error.companionUsage = usageForTurn();
|
|
502
565
|
finish(reject, error);
|
|
503
566
|
return;
|
|
504
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
|
+
}
|
|
505
577
|
finish(resolve, {
|
|
506
|
-
text:
|
|
578
|
+
text: decodedMessage,
|
|
507
579
|
threadId,
|
|
508
580
|
turnId: turn.id || null,
|
|
509
|
-
...
|
|
581
|
+
...usageForTurn(),
|
|
510
582
|
});
|
|
511
583
|
}
|
|
512
584
|
}
|
|
@@ -518,9 +590,9 @@ export function createCodexAppServerAdapter({
|
|
|
518
590
|
CODEX_APP_SERVER_METHODS.turnStart,
|
|
519
591
|
{
|
|
520
592
|
threadId,
|
|
521
|
-
input: [{ type: 'text', text:
|
|
593
|
+
input: [{ type: 'text', text: transportPrompt }],
|
|
522
594
|
...(model ? { model } : {}),
|
|
523
|
-
...(
|
|
595
|
+
...(transportSchema ? { outputSchema: transportSchema } : {}),
|
|
524
596
|
},
|
|
525
597
|
{ timeoutMs: 30000 },
|
|
526
598
|
)
|
|
@@ -542,6 +614,7 @@ export function createCodexAppServerAdapter({
|
|
|
542
614
|
const threadId = threadsByRun.get(sessionId);
|
|
543
615
|
if (!threadId) return;
|
|
544
616
|
threadsByRun.delete(sessionId);
|
|
617
|
+
threadUsageTotals.delete(threadId);
|
|
545
618
|
if (!client || client.closed) return;
|
|
546
619
|
await client.request(
|
|
547
620
|
CODEX_APP_SERVER_METHODS.threadUnsubscribe,
|
|
@@ -556,11 +629,13 @@ export function createCodexAppServerAdapter({
|
|
|
556
629
|
await client.request(CODEX_APP_SERVER_METHODS.logout, {}, { timeoutMs: 15000 });
|
|
557
630
|
} finally {
|
|
558
631
|
threadsByRun.clear();
|
|
632
|
+
threadUsageTotals.clear();
|
|
559
633
|
}
|
|
560
634
|
}
|
|
561
635
|
|
|
562
636
|
function close() {
|
|
563
637
|
threadsByRun.clear();
|
|
638
|
+
threadUsageTotals.clear();
|
|
564
639
|
listeners.clear();
|
|
565
640
|
client?.close();
|
|
566
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
|
+
}
|