remote-codex 0.11.30 → 0.11.32
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/README.md +12 -4
- package/apps/relay-server/dist/index.js +2956 -164
- package/apps/supervisor-api/dist/index.js +521 -64
- package/apps/supervisor-web/dist/apple-touch-icon.png +0 -0
- package/apps/supervisor-web/dist/assets/index-CGHHTNkM.js +21 -0
- package/apps/supervisor-web/dist/assets/index-CdjTdnJt.css +1 -0
- package/apps/supervisor-web/dist/assets/thread-ui-B9eC2H4u.js +3677 -0
- package/apps/supervisor-web/dist/favicon-16x16.png +0 -0
- package/apps/supervisor-web/dist/favicon-32x32.png +0 -0
- package/apps/supervisor-web/dist/favicon-48x48.png +0 -0
- package/apps/supervisor-web/dist/icon-192.png +0 -0
- package/apps/supervisor-web/dist/icon-512.png +0 -0
- package/apps/supervisor-web/dist/index.html +10 -4
- package/apps/supervisor-web/dist/remote-codex-icon.png +0 -0
- package/apps/supervisor-web/dist/site.webmanifest +19 -0
- package/bin/remote-codex.mjs +6 -3
- package/config/codex-model-pricing.json +44 -0
- package/package.json +1 -1
- package/packages/agent-runtime/src/model-pricing.ts +66 -6
- package/packages/agent-runtime/src/types.ts +13 -0
- package/packages/claude/src/runtimeAdapter.test.ts +64 -0
- package/packages/claude/src/runtimeAdapter.ts +64 -0
- package/packages/codex/src/appServerManager.ts +16 -0
- package/packages/codex/src/modelPricing.test.ts +84 -0
- package/packages/codex/src/runtimeAdapter.test.ts +44 -0
- package/packages/codex/src/runtimeAdapter.ts +66 -0
- package/packages/codex/src/types.ts +3 -1
- package/packages/db/migrations/0029_thread_turn_delivery.sql +19 -0
- package/packages/db/src/repositories.ts +96 -1
- package/packages/db/src/schema.ts +20 -0
- package/packages/opencode/src/runtimeAdapter.ts +15 -0
- package/packages/shared/src/index.ts +203 -7
- package/scripts/run-web-service.mjs +2 -2
- package/scripts/service-manager.mjs +2 -2
- package/apps/supervisor-web/dist/assets/index-BnpZn_3_.js +0 -6
- package/apps/supervisor-web/dist/assets/index-CJFMmjP5.css +0 -1
- package/apps/supervisor-web/dist/assets/thread-ui-C0VPL4Uk.js +0 -3677
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -3,17 +3,23 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<
|
|
7
|
-
<
|
|
6
|
+
<meta name="theme-color" content="#101722" />
|
|
7
|
+
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
|
8
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
|
9
|
+
<link rel="icon" type="image/png" sizes="48x48" href="/favicon-48x48.png" />
|
|
10
|
+
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
|
11
|
+
<link rel="manifest" href="/site.webmanifest" />
|
|
12
|
+
<title>Remote Codex</title>
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-CGHHTNkM.js"></script>
|
|
8
14
|
<link rel="modulepreload" crossorigin href="/assets/react-vendor-Dfg_6BLf.js">
|
|
9
15
|
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-CuR8GHb0.js">
|
|
10
16
|
<link rel="modulepreload" crossorigin href="/assets/graph-vendor-DVQUpZ8C.js">
|
|
11
17
|
<link rel="modulepreload" crossorigin href="/assets/terminal-vendor-C5bTa-Ka.js">
|
|
12
18
|
<link rel="modulepreload" crossorigin href="/assets/markdown-vendor-RZk8L7-L.js">
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/thread-ui-
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/assets/thread-ui-B9eC2H4u.js">
|
|
14
20
|
<link rel="stylesheet" crossorigin href="/assets/graph-vendor-C5ap-Sga.css">
|
|
15
21
|
<link rel="stylesheet" crossorigin href="/assets/terminal-vendor-Beg8tuEN.css">
|
|
16
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
22
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CdjTdnJt.css">
|
|
17
23
|
</head>
|
|
18
24
|
<body class="bg-stone-950">
|
|
19
25
|
<div id="root"></div>
|
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Remote Codex",
|
|
3
|
+
"short_name": "Remote Codex",
|
|
4
|
+
"icons": [
|
|
5
|
+
{
|
|
6
|
+
"src": "/icon-192.png",
|
|
7
|
+
"sizes": "192x192",
|
|
8
|
+
"type": "image/png"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"src": "/icon-512.png",
|
|
12
|
+
"sizes": "512x512",
|
|
13
|
+
"type": "image/png"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"theme_color": "#101722",
|
|
17
|
+
"background_color": "#101722",
|
|
18
|
+
"display": "standalone"
|
|
19
|
+
}
|
package/bin/remote-codex.mjs
CHANGED
|
@@ -851,9 +851,9 @@ What it starts:
|
|
|
851
851
|
API http://SERVICE_API_HOST:SERVICE_API_PORT
|
|
852
852
|
|
|
853
853
|
Environment:
|
|
854
|
-
SERVICE_HOST Web listen host. Default
|
|
854
|
+
SERVICE_HOST Web listen host. Default 0.0.0.0.
|
|
855
855
|
SERVICE_PORT Web listen port. Default ${defaultServicePort}.
|
|
856
|
-
SERVICE_API_HOST API listen host. Default
|
|
856
|
+
SERVICE_API_HOST API listen host. Default 0.0.0.0.
|
|
857
857
|
SERVICE_API_PORT API listen port. Default ${defaultApiPort}.
|
|
858
858
|
REMOTE_CODEX_SERVICE_DIR Service state/log directory.
|
|
859
859
|
LOG_LEVEL API log level. Default warn for service mode.
|
|
@@ -871,7 +871,10 @@ Supervisor configuration forwarded to the API:
|
|
|
871
871
|
OPENCODE_COMMAND OpenCode executable. Default opencode.
|
|
872
872
|
|
|
873
873
|
Example:
|
|
874
|
-
|
|
874
|
+
remote-codex start
|
|
875
|
+
|
|
876
|
+
Restrict the service to this machine:
|
|
877
|
+
SERVICE_HOST=127.0.0.1 SERVICE_API_HOST=127.0.0.1 remote-codex start
|
|
875
878
|
|
|
876
879
|
Expose over Tailscale after start:
|
|
877
880
|
tailscale serve --bg http://127.0.0.1:${defaultServicePort}
|
|
@@ -9,6 +9,50 @@
|
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
11
|
"models": {
|
|
12
|
+
"gpt-5.6": {
|
|
13
|
+
"inputUsdPerMillion": 5,
|
|
14
|
+
"cachedInputUsdPerMillion": 0.5,
|
|
15
|
+
"cacheWriteInputUsdPerMillion": 6.25,
|
|
16
|
+
"outputUsdPerMillion": 30,
|
|
17
|
+
"supportsFastMode": true,
|
|
18
|
+
"contextWindowTokens": 353400,
|
|
19
|
+
"longContextThresholdTokens": 272000,
|
|
20
|
+
"longContextInputMultiplier": 2,
|
|
21
|
+
"longContextOutputMultiplier": 1.5
|
|
22
|
+
},
|
|
23
|
+
"gpt-5.6-sol": {
|
|
24
|
+
"inputUsdPerMillion": 5,
|
|
25
|
+
"cachedInputUsdPerMillion": 0.5,
|
|
26
|
+
"cacheWriteInputUsdPerMillion": 6.25,
|
|
27
|
+
"outputUsdPerMillion": 30,
|
|
28
|
+
"supportsFastMode": true,
|
|
29
|
+
"contextWindowTokens": 353400,
|
|
30
|
+
"longContextThresholdTokens": 272000,
|
|
31
|
+
"longContextInputMultiplier": 2,
|
|
32
|
+
"longContextOutputMultiplier": 1.5
|
|
33
|
+
},
|
|
34
|
+
"gpt-5.6-terra": {
|
|
35
|
+
"inputUsdPerMillion": 2.5,
|
|
36
|
+
"cachedInputUsdPerMillion": 0.25,
|
|
37
|
+
"cacheWriteInputUsdPerMillion": 3.125,
|
|
38
|
+
"outputUsdPerMillion": 15,
|
|
39
|
+
"supportsFastMode": true,
|
|
40
|
+
"contextWindowTokens": 353400,
|
|
41
|
+
"longContextThresholdTokens": 272000,
|
|
42
|
+
"longContextInputMultiplier": 2,
|
|
43
|
+
"longContextOutputMultiplier": 1.5
|
|
44
|
+
},
|
|
45
|
+
"gpt-5.6-luna": {
|
|
46
|
+
"inputUsdPerMillion": 1,
|
|
47
|
+
"cachedInputUsdPerMillion": 0.1,
|
|
48
|
+
"cacheWriteInputUsdPerMillion": 1.25,
|
|
49
|
+
"outputUsdPerMillion": 6,
|
|
50
|
+
"supportsFastMode": true,
|
|
51
|
+
"contextWindowTokens": 353400,
|
|
52
|
+
"longContextThresholdTokens": 272000,
|
|
53
|
+
"longContextInputMultiplier": 2,
|
|
54
|
+
"longContextOutputMultiplier": 1.5
|
|
55
|
+
},
|
|
12
56
|
"gpt-5.5": {
|
|
13
57
|
"inputUsdPerMillion": 5,
|
|
14
58
|
"cachedInputUsdPerMillion": 0.5,
|
package/package.json
CHANGED
|
@@ -11,10 +11,14 @@ export type PricingTierKey = 'standard' | 'fast';
|
|
|
11
11
|
interface ModelPricingEntry {
|
|
12
12
|
inputUsdPerMillion: number;
|
|
13
13
|
cachedInputUsdPerMillion: number;
|
|
14
|
+
cacheWriteInputUsdPerMillion?: number;
|
|
14
15
|
outputUsdPerMillion: number;
|
|
15
16
|
supportsFastMode: boolean;
|
|
16
17
|
fastMultiplier?: number;
|
|
17
18
|
contextWindowTokens?: number;
|
|
19
|
+
longContextThresholdTokens?: number;
|
|
20
|
+
longContextInputMultiplier?: number;
|
|
21
|
+
longContextOutputMultiplier?: number;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
interface PricingTierConfig {
|
|
@@ -119,10 +123,14 @@ function parsePricingConfig(raw: unknown): PricingConfig {
|
|
|
119
123
|
const entry = value as {
|
|
120
124
|
inputUsdPerMillion?: unknown;
|
|
121
125
|
cachedInputUsdPerMillion?: unknown;
|
|
126
|
+
cacheWriteInputUsdPerMillion?: unknown;
|
|
122
127
|
outputUsdPerMillion?: unknown;
|
|
123
128
|
supportsFastMode?: unknown;
|
|
124
129
|
fastMultiplier?: unknown;
|
|
125
130
|
contextWindowTokens?: unknown;
|
|
131
|
+
longContextThresholdTokens?: unknown;
|
|
132
|
+
longContextInputMultiplier?: unknown;
|
|
133
|
+
longContextOutputMultiplier?: unknown;
|
|
126
134
|
};
|
|
127
135
|
|
|
128
136
|
if (
|
|
@@ -133,6 +141,12 @@ function parsePricingConfig(raw: unknown): PricingConfig {
|
|
|
133
141
|
) {
|
|
134
142
|
throw new Error(`Pricing config model "${modelKey}" has invalid fields.`);
|
|
135
143
|
}
|
|
144
|
+
if (
|
|
145
|
+
entry.cacheWriteInputUsdPerMillion !== undefined &&
|
|
146
|
+
!isPositiveNumber(entry.cacheWriteInputUsdPerMillion)
|
|
147
|
+
) {
|
|
148
|
+
throw new Error(`Pricing config model "${modelKey}" cacheWriteInputUsdPerMillion must be a non-negative number.`);
|
|
149
|
+
}
|
|
136
150
|
if (
|
|
137
151
|
entry.fastMultiplier !== undefined &&
|
|
138
152
|
!isPositiveNumber(entry.fastMultiplier)
|
|
@@ -145,10 +159,22 @@ function parsePricingConfig(raw: unknown): PricingConfig {
|
|
|
145
159
|
) {
|
|
146
160
|
throw new Error(`Pricing config model "${modelKey}" contextWindowTokens must be a non-negative number.`);
|
|
147
161
|
}
|
|
162
|
+
for (const field of [
|
|
163
|
+
'longContextThresholdTokens',
|
|
164
|
+
'longContextInputMultiplier',
|
|
165
|
+
'longContextOutputMultiplier',
|
|
166
|
+
] as const) {
|
|
167
|
+
if (entry[field] !== undefined && !isPositiveNumber(entry[field])) {
|
|
168
|
+
throw new Error(`Pricing config model "${modelKey}" ${field} must be a non-negative number.`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
148
171
|
|
|
149
172
|
models[modelKey] = {
|
|
150
173
|
inputUsdPerMillion: entry.inputUsdPerMillion as number,
|
|
151
174
|
cachedInputUsdPerMillion: entry.cachedInputUsdPerMillion as number,
|
|
175
|
+
...(entry.cacheWriteInputUsdPerMillion !== undefined
|
|
176
|
+
? { cacheWriteInputUsdPerMillion: entry.cacheWriteInputUsdPerMillion as number }
|
|
177
|
+
: {}),
|
|
152
178
|
outputUsdPerMillion: entry.outputUsdPerMillion as number,
|
|
153
179
|
supportsFastMode: entry.supportsFastMode,
|
|
154
180
|
...(entry.fastMultiplier !== undefined
|
|
@@ -157,6 +183,15 @@ function parsePricingConfig(raw: unknown): PricingConfig {
|
|
|
157
183
|
...(entry.contextWindowTokens !== undefined
|
|
158
184
|
? { contextWindowTokens: entry.contextWindowTokens as number }
|
|
159
185
|
: {}),
|
|
186
|
+
...(entry.longContextThresholdTokens !== undefined
|
|
187
|
+
? { longContextThresholdTokens: entry.longContextThresholdTokens as number }
|
|
188
|
+
: {}),
|
|
189
|
+
...(entry.longContextInputMultiplier !== undefined
|
|
190
|
+
? { longContextInputMultiplier: entry.longContextInputMultiplier as number }
|
|
191
|
+
: {}),
|
|
192
|
+
...(entry.longContextOutputMultiplier !== undefined
|
|
193
|
+
? { longContextOutputMultiplier: entry.longContextOutputMultiplier as number }
|
|
194
|
+
: {}),
|
|
160
195
|
};
|
|
161
196
|
}
|
|
162
197
|
|
|
@@ -293,24 +328,48 @@ export function estimateTurnPrice(
|
|
|
293
328
|
}
|
|
294
329
|
|
|
295
330
|
const nonCachedInputTokens = Math.max(
|
|
296
|
-
usage.total.inputTokens -
|
|
331
|
+
usage.total.inputTokens -
|
|
332
|
+
usage.total.cachedInputTokens -
|
|
333
|
+
(usage.total.cacheWriteInputTokens ?? 0),
|
|
297
334
|
0,
|
|
298
335
|
);
|
|
299
336
|
const cachedInputTokens = Math.max(usage.total.cachedInputTokens, 0);
|
|
337
|
+
const cacheWriteInputTokens = Math.max(
|
|
338
|
+
usage.total.cacheWriteInputTokens ?? 0,
|
|
339
|
+
0,
|
|
340
|
+
);
|
|
300
341
|
const outputTokens = Math.max(usage.total.outputTokens, 0);
|
|
301
|
-
const
|
|
342
|
+
const tierMultiplier =
|
|
302
343
|
tierKey === 'fast' && modelPricing.fastMultiplier !== undefined
|
|
303
344
|
? modelPricing.fastMultiplier
|
|
304
345
|
: tier.multiplier;
|
|
346
|
+
const usesLongContextPricing =
|
|
347
|
+
typeof modelPricing.longContextThresholdTokens === 'number' &&
|
|
348
|
+
usage.total.inputTokens > modelPricing.longContextThresholdTokens;
|
|
349
|
+
const inputMultiplier =
|
|
350
|
+
tierMultiplier *
|
|
351
|
+
(usesLongContextPricing
|
|
352
|
+
? modelPricing.longContextInputMultiplier ?? 1
|
|
353
|
+
: 1);
|
|
354
|
+
const outputMultiplier =
|
|
355
|
+
tierMultiplier *
|
|
356
|
+
(usesLongContextPricing
|
|
357
|
+
? modelPricing.longContextOutputMultiplier ?? 1
|
|
358
|
+
: 1);
|
|
305
359
|
|
|
306
360
|
const inputUsd =
|
|
307
|
-
(nonCachedInputTokens * modelPricing.inputUsdPerMillion *
|
|
361
|
+
(nonCachedInputTokens * modelPricing.inputUsdPerMillion * inputMultiplier) /
|
|
308
362
|
TOKEN_PRICE_DENOMINATOR;
|
|
309
363
|
const cachedInputUsd =
|
|
310
|
-
(cachedInputTokens * modelPricing.cachedInputUsdPerMillion *
|
|
364
|
+
(cachedInputTokens * modelPricing.cachedInputUsdPerMillion * inputMultiplier) /
|
|
365
|
+
TOKEN_PRICE_DENOMINATOR;
|
|
366
|
+
const cacheWriteInputUsd =
|
|
367
|
+
(cacheWriteInputTokens *
|
|
368
|
+
(modelPricing.cacheWriteInputUsdPerMillion ?? modelPricing.inputUsdPerMillion) *
|
|
369
|
+
inputMultiplier) /
|
|
311
370
|
TOKEN_PRICE_DENOMINATOR;
|
|
312
371
|
const outputUsd =
|
|
313
|
-
(outputTokens * modelPricing.outputUsdPerMillion *
|
|
372
|
+
(outputTokens * modelPricing.outputUsdPerMillion * outputMultiplier) /
|
|
314
373
|
TOKEN_PRICE_DENOMINATOR;
|
|
315
374
|
|
|
316
375
|
return {
|
|
@@ -319,7 +378,8 @@ export function estimateTurnPrice(
|
|
|
319
378
|
currency: pricingConfig.currency,
|
|
320
379
|
inputUsd,
|
|
321
380
|
cachedInputUsd,
|
|
381
|
+
cacheWriteInputUsd,
|
|
322
382
|
outputUsd,
|
|
323
|
-
totalUsd: inputUsd + cachedInputUsd + outputUsd,
|
|
383
|
+
totalUsd: inputUsd + cachedInputUsd + cacheWriteInputUsd + outputUsd,
|
|
324
384
|
};
|
|
325
385
|
}
|
|
@@ -582,4 +582,17 @@ export interface AgentRuntime extends EventEmitter {
|
|
|
582
582
|
getGoal?(providerSessionId: string): Promise<AgentGoal | null>;
|
|
583
583
|
setGoal?(input: SetAgentGoalInput): Promise<AgentGoal>;
|
|
584
584
|
clearGoal?(providerSessionId: string): Promise<boolean>;
|
|
585
|
+
getSubscriptionUsage?(): Promise<{
|
|
586
|
+
provider: 'codex' | 'claude';
|
|
587
|
+
authKind: 'subscription' | 'apiKey' | 'unknown';
|
|
588
|
+
observedAt: string;
|
|
589
|
+
stale: boolean;
|
|
590
|
+
windows: Array<{
|
|
591
|
+
id: string;
|
|
592
|
+
durationMinutes: number | null;
|
|
593
|
+
label: string;
|
|
594
|
+
usedPercent: number;
|
|
595
|
+
resetsAt: string | null;
|
|
596
|
+
}>;
|
|
597
|
+
} | null>;
|
|
585
598
|
}
|
|
@@ -208,6 +208,70 @@ function makeAdapter(
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
describe('ClaudeRuntimeAdapter', () => {
|
|
211
|
+
it('captures Claude subscription rate-limit windows from SDK events', async () => {
|
|
212
|
+
const adapter = makeAdapter(() => [
|
|
213
|
+
systemInit(),
|
|
214
|
+
{
|
|
215
|
+
type: 'rate_limit_event',
|
|
216
|
+
rate_limit_info: {
|
|
217
|
+
status: 'allowed',
|
|
218
|
+
rateLimitType: 'five_hour',
|
|
219
|
+
utilization: 0.23,
|
|
220
|
+
resetsAt: 1_800_000_000,
|
|
221
|
+
},
|
|
222
|
+
uuid: '00000000-0000-4000-8000-000000000003',
|
|
223
|
+
session_id: 'claude-session-1',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
type: 'rate_limit_event',
|
|
227
|
+
rate_limit_info: {
|
|
228
|
+
status: 'allowed_warning',
|
|
229
|
+
rateLimitType: 'seven_day',
|
|
230
|
+
utilization: 0.84,
|
|
231
|
+
resetsAt: 1_800_086_400,
|
|
232
|
+
},
|
|
233
|
+
uuid: '00000000-0000-4000-8000-000000000004',
|
|
234
|
+
session_id: 'claude-session-1',
|
|
235
|
+
},
|
|
236
|
+
result(),
|
|
237
|
+
]);
|
|
238
|
+
|
|
239
|
+
await adapter.start();
|
|
240
|
+
await expect(adapter.getSubscriptionUsage()).resolves.toMatchObject({
|
|
241
|
+
provider: 'claude',
|
|
242
|
+
authKind: 'unknown',
|
|
243
|
+
windows: [],
|
|
244
|
+
});
|
|
245
|
+
await adapter.startSession({
|
|
246
|
+
cwd: '/tmp/workspace',
|
|
247
|
+
model: 'sonnet',
|
|
248
|
+
approvalMode: 'guarded',
|
|
249
|
+
sandboxMode: 'workspace-write',
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
await expect(adapter.getSubscriptionUsage()).resolves.toMatchObject({
|
|
253
|
+
provider: 'claude',
|
|
254
|
+
authKind: 'subscription',
|
|
255
|
+
stale: false,
|
|
256
|
+
windows: [
|
|
257
|
+
{
|
|
258
|
+
id: 'five_hour',
|
|
259
|
+
durationMinutes: 300,
|
|
260
|
+
label: '5h',
|
|
261
|
+
usedPercent: 23,
|
|
262
|
+
resetsAt: new Date(1_800_000_000 * 1000).toISOString(),
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
id: 'seven_day',
|
|
266
|
+
durationMinutes: 10_080,
|
|
267
|
+
label: '7d',
|
|
268
|
+
usedPercent: 84,
|
|
269
|
+
resetsAt: new Date(1_800_086_400 * 1000).toISOString(),
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
211
275
|
it('passes the configured Claude executable to the SDK', async () => {
|
|
212
276
|
const sdkOptions: Record<string, unknown>[] = [];
|
|
213
277
|
const adapter = new ClaudeRuntimeAdapter({
|
|
@@ -119,6 +119,19 @@ interface SDKMessage {
|
|
|
119
119
|
modelUsage?: unknown;
|
|
120
120
|
errors?: string[];
|
|
121
121
|
stop_reason?: string;
|
|
122
|
+
rate_limit_info?: SDKRateLimitInfo;
|
|
123
|
+
}
|
|
124
|
+
interface SDKRateLimitInfo {
|
|
125
|
+
status: 'allowed' | 'allowed_warning' | 'rejected';
|
|
126
|
+
resetsAt?: number;
|
|
127
|
+
rateLimitType?:
|
|
128
|
+
| 'five_hour'
|
|
129
|
+
| 'seven_day'
|
|
130
|
+
| 'seven_day_opus'
|
|
131
|
+
| 'seven_day_sonnet'
|
|
132
|
+
| 'seven_day_overage_included'
|
|
133
|
+
| 'overage';
|
|
134
|
+
utilization?: number;
|
|
122
135
|
}
|
|
123
136
|
interface SDKUserMessage {
|
|
124
137
|
type: 'user';
|
|
@@ -364,6 +377,7 @@ interface ClaudeTokenUsageBreakdown {
|
|
|
364
377
|
totalTokens: number;
|
|
365
378
|
inputTokens: number;
|
|
366
379
|
cachedInputTokens: number;
|
|
380
|
+
cacheWriteInputTokens: number;
|
|
367
381
|
outputTokens: number;
|
|
368
382
|
reasoningOutputTokens: number;
|
|
369
383
|
}
|
|
@@ -792,6 +806,8 @@ function addClaudeUsage(
|
|
|
792
806
|
totalTokens: left.totalTokens + right.totalTokens,
|
|
793
807
|
inputTokens: left.inputTokens + right.inputTokens,
|
|
794
808
|
cachedInputTokens: left.cachedInputTokens + right.cachedInputTokens,
|
|
809
|
+
cacheWriteInputTokens:
|
|
810
|
+
left.cacheWriteInputTokens + right.cacheWriteInputTokens,
|
|
795
811
|
outputTokens: left.outputTokens + right.outputTokens,
|
|
796
812
|
reasoningOutputTokens: left.reasoningOutputTokens + right.reasoningOutputTokens,
|
|
797
813
|
};
|
|
@@ -821,6 +837,7 @@ function normalizeClaudeUsage(value: unknown): ClaudeTokenUsageBreakdown | null
|
|
|
821
837
|
totalTokens,
|
|
822
838
|
inputTokens,
|
|
823
839
|
cachedInputTokens: cacheReadInputTokens,
|
|
840
|
+
cacheWriteInputTokens: cacheCreationInputTokens,
|
|
824
841
|
outputTokens,
|
|
825
842
|
reasoningOutputTokens: 0,
|
|
826
843
|
};
|
|
@@ -1218,6 +1235,11 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
1218
1235
|
private readonly sessionModels = new Map<string, string | null>();
|
|
1219
1236
|
private readonly sessionApprovalModes = new Map<string, StartAgentSessionInput['approvalMode']>();
|
|
1220
1237
|
private readonly liveUserPrompts = new Map<string, Map<string, string>>();
|
|
1238
|
+
private readonly subscriptionUsageWindows = new Map<
|
|
1239
|
+
'five_hour' | 'seven_day',
|
|
1240
|
+
{ usedPercent: number; resetsAt: string | null }
|
|
1241
|
+
>();
|
|
1242
|
+
private subscriptionUsageObservedAt: string | null = null;
|
|
1221
1243
|
private readonly clientApp: string;
|
|
1222
1244
|
private sdkLoadError: string | null = null;
|
|
1223
1245
|
|
|
@@ -1238,6 +1260,46 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
1238
1260
|
return { ...this.status };
|
|
1239
1261
|
}
|
|
1240
1262
|
|
|
1263
|
+
async getSubscriptionUsage() {
|
|
1264
|
+
const observedAt = this.subscriptionUsageObservedAt ?? new Date().toISOString();
|
|
1265
|
+
const windows = [...this.subscriptionUsageWindows.entries()].map(([id, window]) => ({
|
|
1266
|
+
id,
|
|
1267
|
+
durationMinutes: id === 'five_hour' ? 300 : 10_080,
|
|
1268
|
+
label: id === 'five_hour' ? '5h' : '7d',
|
|
1269
|
+
usedPercent: window.usedPercent,
|
|
1270
|
+
resetsAt: window.resetsAt,
|
|
1271
|
+
}));
|
|
1272
|
+
return {
|
|
1273
|
+
provider: 'claude' as const,
|
|
1274
|
+
authKind: windows.length > 0 ? 'subscription' as const : 'unknown' as const,
|
|
1275
|
+
observedAt,
|
|
1276
|
+
stale: false,
|
|
1277
|
+
windows,
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
private captureRateLimit(message: SDKMessage) {
|
|
1282
|
+
if (message.type !== 'rate_limit_event') {
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
const info = message.rate_limit_info;
|
|
1286
|
+
if (
|
|
1287
|
+
!info
|
|
1288
|
+
|| (info.rateLimitType !== 'five_hour' && info.rateLimitType !== 'seven_day')
|
|
1289
|
+
|| typeof info.utilization !== 'number'
|
|
1290
|
+
|| !Number.isFinite(info.utilization)
|
|
1291
|
+
) {
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
this.subscriptionUsageWindows.set(info.rateLimitType, {
|
|
1295
|
+
usedPercent: Math.max(0, Math.min(100, info.utilization * 100)),
|
|
1296
|
+
resetsAt: typeof info.resetsAt === 'number'
|
|
1297
|
+
? new Date(info.resetsAt * 1000).toISOString()
|
|
1298
|
+
: null,
|
|
1299
|
+
});
|
|
1300
|
+
this.subscriptionUsageObservedAt = new Date().toISOString();
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1241
1303
|
private updateToolboxItemsFromSystemInit(message: SDKMessage) {
|
|
1242
1304
|
this.managementSchema.toolboxItems = buildClaudeToolboxItems(
|
|
1243
1305
|
normalizeClaudeSlashCommands(message.slash_commands),
|
|
@@ -1404,6 +1466,7 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
1404
1466
|
try {
|
|
1405
1467
|
for await (const message of query) {
|
|
1406
1468
|
rawMessages.push(message);
|
|
1469
|
+
this.captureRateLimit(message);
|
|
1407
1470
|
if (message.type === 'system' && message.subtype === 'init') {
|
|
1408
1471
|
this.updateToolboxItemsFromSystemInit(message);
|
|
1409
1472
|
const sessionId = message.session_id;
|
|
@@ -1695,6 +1758,7 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
1695
1758
|
try {
|
|
1696
1759
|
for await (const message of state.query) {
|
|
1697
1760
|
rawMessages.push(message);
|
|
1761
|
+
this.captureRateLimit(message);
|
|
1698
1762
|
this.consumeMessage(state, message);
|
|
1699
1763
|
const status = queryResultStatus(message);
|
|
1700
1764
|
if (status) {
|
|
@@ -320,6 +320,22 @@ export class CodexAppServerManager extends EventEmitter {
|
|
|
320
320
|
return response.data.map(mapModel);
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
async readAccount() {
|
|
324
|
+
await this.ensureReady();
|
|
325
|
+
return this.client!.request<{
|
|
326
|
+
account: { type: string } | null;
|
|
327
|
+
requiresOpenaiAuth: boolean;
|
|
328
|
+
}>('account/read', { refreshToken: false });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async readAccountRateLimits() {
|
|
332
|
+
await this.ensureReady();
|
|
333
|
+
return this.client!.request<{
|
|
334
|
+
rateLimits: unknown;
|
|
335
|
+
rateLimitsByLimitId?: Record<string, unknown> | null;
|
|
336
|
+
}>('account/rateLimits/read', null);
|
|
337
|
+
}
|
|
338
|
+
|
|
323
339
|
async listThreads(): Promise<CodexThreadRecord[]> {
|
|
324
340
|
await this.ensureReady();
|
|
325
341
|
const response = await this.client!.request<{ data: any[] }>('thread/list', {
|
|
@@ -52,6 +52,90 @@ describe('modelPricing', () => {
|
|
|
52
52
|
expect(estimate?.totalUsd).toBeCloseTo(0.05025, 10);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
+
it('prices GPT-5.6 aliases and cache writes with the published rates', () => {
|
|
56
|
+
const usage = {
|
|
57
|
+
...sampleUsage,
|
|
58
|
+
total: {
|
|
59
|
+
...sampleUsage.total,
|
|
60
|
+
cacheWriteInputTokens: 200,
|
|
61
|
+
},
|
|
62
|
+
last: {
|
|
63
|
+
...sampleUsage.last,
|
|
64
|
+
cacheWriteInputTokens: 200,
|
|
65
|
+
},
|
|
66
|
+
modelContextWindow: 353_400,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
expect(contextWindowForModel('gpt-5.6')).toBe(353_400);
|
|
70
|
+
expect(contextWindowForModel('openai/gpt-5.6-sol')).toBe(353_400);
|
|
71
|
+
expect(supportsFastMode('gpt-5.6-terra')).toBe(true);
|
|
72
|
+
expect(buildTurnPricingSnapshot('gpt-5.6-luna', false)).toEqual({
|
|
73
|
+
pricingModelKey: 'gpt-5.6-luna',
|
|
74
|
+
pricingTierKey: 'standard',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const solEstimate = estimateTurnPrice(usage, {
|
|
78
|
+
pricingModelKey: 'gpt-5.6-sol',
|
|
79
|
+
pricingTierKey: 'standard',
|
|
80
|
+
});
|
|
81
|
+
expect(solEstimate).toMatchObject({
|
|
82
|
+
pricingModelKey: 'gpt-5.6-sol',
|
|
83
|
+
inputUsd: 0.004,
|
|
84
|
+
cachedInputUsd: 0.00025,
|
|
85
|
+
cacheWriteInputUsd: 0.00125,
|
|
86
|
+
outputUsd: 0.045,
|
|
87
|
+
});
|
|
88
|
+
expect(solEstimate?.totalUsd).toBeCloseTo(0.0505, 10);
|
|
89
|
+
|
|
90
|
+
const terraEstimate = estimateTurnPrice(usage, {
|
|
91
|
+
pricingModelKey: 'gpt-5.6-terra',
|
|
92
|
+
pricingTierKey: 'standard',
|
|
93
|
+
});
|
|
94
|
+
expect(terraEstimate?.cacheWriteInputUsd).toBeCloseTo(0.000625, 10);
|
|
95
|
+
|
|
96
|
+
const lunaEstimate = estimateTurnPrice(usage, {
|
|
97
|
+
pricingModelKey: 'gpt-5.6-luna',
|
|
98
|
+
pricingTierKey: 'standard',
|
|
99
|
+
});
|
|
100
|
+
expect(lunaEstimate?.cacheWriteInputUsd).toBeCloseTo(0.00025, 10);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('applies GPT-5.6 long-context rates above 272K input tokens', () => {
|
|
104
|
+
const estimate = estimateTurnPrice(
|
|
105
|
+
{
|
|
106
|
+
total: {
|
|
107
|
+
totalTokens: 301_000,
|
|
108
|
+
inputTokens: 300_000,
|
|
109
|
+
cachedInputTokens: 0,
|
|
110
|
+
cacheWriteInputTokens: 0,
|
|
111
|
+
outputTokens: 1_000,
|
|
112
|
+
reasoningOutputTokens: 0,
|
|
113
|
+
},
|
|
114
|
+
last: {
|
|
115
|
+
totalTokens: 301_000,
|
|
116
|
+
inputTokens: 300_000,
|
|
117
|
+
cachedInputTokens: 0,
|
|
118
|
+
cacheWriteInputTokens: 0,
|
|
119
|
+
outputTokens: 1_000,
|
|
120
|
+
reasoningOutputTokens: 0,
|
|
121
|
+
},
|
|
122
|
+
modelContextWindow: 353_400,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
pricingModelKey: 'gpt-5.6-sol',
|
|
126
|
+
pricingTierKey: 'standard',
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
expect(estimate).toMatchObject({
|
|
131
|
+
inputUsd: 3,
|
|
132
|
+
cachedInputUsd: 0,
|
|
133
|
+
cacheWriteInputUsd: 0,
|
|
134
|
+
outputUsd: 0.045,
|
|
135
|
+
});
|
|
136
|
+
expect(estimate?.totalUsd).toBeCloseTo(3.045, 10);
|
|
137
|
+
});
|
|
138
|
+
|
|
55
139
|
it('uses the gpt-5.5-specific fast multiplier and marks it fast-capable', () => {
|
|
56
140
|
expect(supportsFastMode('gpt-5.5')).toBe(true);
|
|
57
141
|
expect(contextWindowForModel('gpt-5.5')).toBe(272000);
|
|
@@ -27,6 +27,22 @@ class FakeCodexManager extends EventEmitter {
|
|
|
27
27
|
items: [],
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
async readAccount() {
|
|
32
|
+
return { account: { type: 'chatgpt' }, requiresOpenaiAuth: true };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async readAccountRateLimits() {
|
|
36
|
+
return {
|
|
37
|
+
rateLimits: {},
|
|
38
|
+
rateLimitsByLimitId: {
|
|
39
|
+
codex: {
|
|
40
|
+
primary: { usedPercent: 40, windowDurationMins: 300, resetsAt: 1_800_000_000 },
|
|
41
|
+
secondary: { usedPercent: 75, windowDurationMins: 10_080, resetsAt: 1_800_604_800 },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
30
46
|
}
|
|
31
47
|
|
|
32
48
|
describe('CodexRuntimeAdapter', () => {
|
|
@@ -73,4 +89,32 @@ describe('CodexRuntimeAdapter', () => {
|
|
|
73
89
|
{ type: 'text', text: '.', text_elements: [] },
|
|
74
90
|
]);
|
|
75
91
|
});
|
|
92
|
+
|
|
93
|
+
it('maps ChatGPT rate-limit windows without assuming fixed durations', async () => {
|
|
94
|
+
const adapter = new CodexRuntimeAdapter(new FakeCodexManager() as never);
|
|
95
|
+
|
|
96
|
+
await expect(adapter.getSubscriptionUsage()).resolves.toMatchObject({
|
|
97
|
+
provider: 'codex',
|
|
98
|
+
authKind: 'subscription',
|
|
99
|
+
stale: false,
|
|
100
|
+
windows: [
|
|
101
|
+
{ id: 'primary', label: '5h', durationMinutes: 300, usedPercent: 40 },
|
|
102
|
+
{ id: 'secondary', label: '7d', durationMinutes: 10_080, usedPercent: 75 },
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('hides subscription windows for API-key authentication', async () => {
|
|
108
|
+
const manager = new FakeCodexManager();
|
|
109
|
+
manager.readAccount = async () => ({
|
|
110
|
+
account: { type: 'apiKey' },
|
|
111
|
+
requiresOpenaiAuth: true,
|
|
112
|
+
});
|
|
113
|
+
const adapter = new CodexRuntimeAdapter(manager as never);
|
|
114
|
+
|
|
115
|
+
await expect(adapter.getSubscriptionUsage()).resolves.toMatchObject({
|
|
116
|
+
authKind: 'apiKey',
|
|
117
|
+
windows: [],
|
|
118
|
+
});
|
|
119
|
+
});
|
|
76
120
|
});
|