remote-codex 0.1.9 → 0.11.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/apps/supervisor-api/dist/index.js +11942 -6101
- package/apps/supervisor-web/dist/assets/{highlighted-body-OFNGDK62-BFD4Ytvg.js → highlighted-body-OFNGDK62-ChrwAL9u.js} +1 -1
- package/apps/supervisor-web/dist/assets/index-DHf2HOXx.js +381 -0
- package/apps/supervisor-web/dist/assets/index-DpWxXCgt.css +32 -0
- package/apps/supervisor-web/dist/assets/{xterm-CukFWbxr.js → xterm-D4sevve4.js} +1 -1
- package/apps/supervisor-web/dist/index.html +2 -2
- package/config/codex-model-pricing.json +63 -0
- package/package.json +5 -2
- package/packages/agent-runtime/src/index.ts +4 -0
- package/packages/agent-runtime/src/management-errors.ts +11 -0
- package/packages/agent-runtime/src/model-pricing.ts +312 -0
- package/packages/agent-runtime/src/registry.ts +19 -4
- package/packages/agent-runtime/src/runtime-errors.ts +97 -0
- package/packages/agent-runtime/src/types.ts +50 -4
- package/packages/agent-runtime/src/unavailable-runtime.ts +169 -0
- package/packages/claude/src/historyItems.ts +693 -0
- package/packages/claude/src/index.ts +2 -0
- package/packages/claude/src/runtimeAdapter.test.ts +2138 -0
- package/packages/claude/src/runtimeAdapter.ts +2145 -0
- package/packages/codex/src/appServerManager.ts +12 -3
- package/packages/codex/src/historyItems.test.ts +110 -0
- package/packages/codex/src/historyItems.ts +97 -16
- package/packages/codex/src/hookHistory.test.ts +59 -0
- package/packages/codex/src/index.ts +7 -0
- package/packages/codex/src/local-session-store.ts +390 -0
- package/packages/codex/src/management/codex-management-service.ts +454 -0
- package/packages/codex/src/management/codexHostConfig.test.ts +88 -0
- package/packages/codex/src/management/codexHostConfig.ts +188 -0
- package/packages/codex/src/management/errors.ts +20 -0
- package/packages/codex/src/modelPricing.test.ts +184 -0
- package/packages/codex/src/modelPricing.ts +9 -0
- package/packages/codex/src/runtime-errors.test.ts +72 -0
- package/packages/codex/src/runtime-errors.ts +37 -0
- package/packages/codex/src/runtimeAdapter.ts +25 -2
- package/packages/codex/src/thread-title.ts +1 -0
- package/packages/db/src/repositories.ts +30 -0
- package/packages/opencode/src/historyItems.test.ts +504 -0
- package/packages/opencode/src/historyItems.ts +896 -0
- package/packages/opencode/src/index.ts +2 -0
- package/packages/opencode/src/runtimeAdapter.test.ts +1355 -0
- package/packages/opencode/src/runtimeAdapter.ts +1469 -0
- package/packages/shared/src/agent-providers.ts +56 -0
- package/packages/shared/src/index.ts +174 -35
- package/apps/supervisor-web/dist/assets/index-CbIt0KnL.css +0 -32
- package/apps/supervisor-web/dist/assets/index-Rd2EBQac.js +0 -377
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import fsp from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export type CodexServiceTier = 'fast' | null;
|
|
6
|
+
const SERVICE_TIER_LINE_GLOBAL_PATTERN =
|
|
7
|
+
/^\s*service_tier\s*=\s*("fast"|"flex"|'fast'|'flex').*\n?/gm;
|
|
8
|
+
|
|
9
|
+
function resolveCodexConfigPath(codexHome: string) {
|
|
10
|
+
return path.join(codexHome, 'config.toml');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseCodexServiceTier(content: string): CodexServiceTier {
|
|
14
|
+
const match = content.match(
|
|
15
|
+
/^\s*service_tier\s*=\s*["'](?<tier>fast)["']\s*$/m,
|
|
16
|
+
);
|
|
17
|
+
const tier = match?.groups?.tier;
|
|
18
|
+
return tier === 'fast' ? tier : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isFastModeEnabledFromConfig(content: string) {
|
|
22
|
+
return parseCodexServiceTier(content) === 'fast';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function readCodexFastModeSync(codexHome: string) {
|
|
26
|
+
try {
|
|
27
|
+
const content = fs.readFileSync(resolveCodexConfigPath(codexHome), 'utf8');
|
|
28
|
+
return isFastModeEnabledFromConfig(content);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function upsertCodexServiceTier(content: string, enabled: boolean) {
|
|
38
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
39
|
+
const withoutServiceTier = normalized.replace(
|
|
40
|
+
SERVICE_TIER_LINE_GLOBAL_PATTERN,
|
|
41
|
+
'',
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (!enabled) {
|
|
45
|
+
return withoutServiceTier.replace(/\n{3,}/g, '\n\n').trimEnd()
|
|
46
|
+
+ (withoutServiceTier.trim() ? '\n' : '');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const nextLine = 'service_tier = "fast"';
|
|
50
|
+
if (!withoutServiceTier.trim()) {
|
|
51
|
+
return `${nextLine}\n`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const firstSectionMatch = withoutServiceTier.match(/^\s*\[[^\]]+\]/m);
|
|
55
|
+
if (!firstSectionMatch || firstSectionMatch.index === undefined) {
|
|
56
|
+
return withoutServiceTier.endsWith('\n')
|
|
57
|
+
? `${withoutServiceTier}${nextLine}\n`
|
|
58
|
+
: `${withoutServiceTier}\n${nextLine}\n`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const beforeFirstSection = withoutServiceTier.slice(0, firstSectionMatch.index).trimEnd();
|
|
62
|
+
const afterFirstSection = withoutServiceTier
|
|
63
|
+
.slice(firstSectionMatch.index)
|
|
64
|
+
.replace(/^\n+/, '');
|
|
65
|
+
return beforeFirstSection
|
|
66
|
+
? `${beforeFirstSection}\n${nextLine}\n${afterFirstSection}`
|
|
67
|
+
: `${nextLine}\n${afterFirstSection}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function writeCodexFastMode(codexHome: string, enabled: boolean) {
|
|
71
|
+
const configPath = resolveCodexConfigPath(codexHome);
|
|
72
|
+
let current = '';
|
|
73
|
+
try {
|
|
74
|
+
current = await fsp.readFile(configPath, 'utf8');
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const next = upsertCodexServiceTier(current, enabled);
|
|
82
|
+
await fsp.mkdir(path.dirname(configPath), { recursive: true });
|
|
83
|
+
await fsp.writeFile(configPath, next, 'utf8');
|
|
84
|
+
return next;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isCodexFeatureEnabledFromConfig(content: string, featureName: string) {
|
|
88
|
+
const escaped = featureName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
89
|
+
const featuresMatch = content.match(/^[^\S\r\n]*\[features\][^\S\r\n]*$/m);
|
|
90
|
+
if (!featuresMatch || featuresMatch.index === undefined) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const sectionStart = featuresMatch.index + featuresMatch[0].length;
|
|
95
|
+
const nextSectionMatch = content
|
|
96
|
+
.slice(sectionStart)
|
|
97
|
+
.match(/^[^\S\r\n]*\[[^\]\r\n]+\][^\S\r\n]*$/m);
|
|
98
|
+
const sectionEnd =
|
|
99
|
+
nextSectionMatch && nextSectionMatch.index !== undefined
|
|
100
|
+
? sectionStart + nextSectionMatch.index
|
|
101
|
+
: content.length;
|
|
102
|
+
const featuresBody = content.slice(sectionStart, sectionEnd);
|
|
103
|
+
return new RegExp(`^\\s*${escaped}\\s*=\\s*true\\s*(?:#.*)?$`, 'm').test(featuresBody);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function readCodexFeatureFlag(
|
|
107
|
+
codexHome: string,
|
|
108
|
+
featureName: string,
|
|
109
|
+
) {
|
|
110
|
+
try {
|
|
111
|
+
const content = await fsp.readFile(resolveCodexConfigPath(codexHome), 'utf8');
|
|
112
|
+
return isCodexFeatureEnabledFromConfig(content, featureName);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function upsertCodexFeatureFlag(
|
|
122
|
+
content: string,
|
|
123
|
+
featureName: string,
|
|
124
|
+
enabled: boolean,
|
|
125
|
+
) {
|
|
126
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
127
|
+
const escaped = featureName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
128
|
+
const featuresMatch = normalized.match(/^[^\S\r\n]*\[features\][^\S\r\n]*$/m);
|
|
129
|
+
const nextLine = `${featureName} = ${enabled ? 'true' : 'false'}`;
|
|
130
|
+
|
|
131
|
+
if (!featuresMatch || featuresMatch.index === undefined) {
|
|
132
|
+
const prefix = normalized.trimEnd();
|
|
133
|
+
return prefix
|
|
134
|
+
? `${prefix}\n\n[features]\n${nextLine}\n`
|
|
135
|
+
: `[features]\n${nextLine}\n`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const sectionStart = featuresMatch.index + featuresMatch[0].length;
|
|
139
|
+
const nextSectionMatch = normalized
|
|
140
|
+
.slice(sectionStart)
|
|
141
|
+
.match(/^[^\S\r\n]*\[[^\]\r\n]+\][^\S\r\n]*$/m);
|
|
142
|
+
const sectionEnd =
|
|
143
|
+
nextSectionMatch && nextSectionMatch.index !== undefined
|
|
144
|
+
? sectionStart + nextSectionMatch.index
|
|
145
|
+
: normalized.length;
|
|
146
|
+
const beforeSectionBody = normalized.slice(0, sectionStart);
|
|
147
|
+
const sectionBody = normalized.slice(sectionStart, sectionEnd);
|
|
148
|
+
const afterSection = normalized.slice(sectionEnd);
|
|
149
|
+
const flagPattern = new RegExp(
|
|
150
|
+
`(^[^\\S\\r\\n]*)${escaped}[^\\S\\r\\n]*=[^\\S\\r\\n]*(true|false)[^\\S\\r\\n]*(?:#.*)?$`,
|
|
151
|
+
'm',
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
if (flagPattern.test(sectionBody)) {
|
|
155
|
+
return (
|
|
156
|
+
beforeSectionBody +
|
|
157
|
+
sectionBody.replace(flagPattern, `$1${nextLine}`) +
|
|
158
|
+
afterSection
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const bodyWithFlag = `${sectionBody.replace(/\n*$/, '')}\n${nextLine}\n`;
|
|
163
|
+
const normalizedAfterSection = afterSection.startsWith('\n')
|
|
164
|
+
? afterSection
|
|
165
|
+
: `\n${afterSection}`;
|
|
166
|
+
return beforeSectionBody + bodyWithFlag + normalizedAfterSection;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function writeCodexFeatureFlag(
|
|
170
|
+
codexHome: string,
|
|
171
|
+
featureName: string,
|
|
172
|
+
enabled: boolean,
|
|
173
|
+
) {
|
|
174
|
+
const configPath = resolveCodexConfigPath(codexHome);
|
|
175
|
+
let current = '';
|
|
176
|
+
try {
|
|
177
|
+
current = await fsp.readFile(configPath, 'utf8');
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const next = upsertCodexFeatureFlag(current, featureName, enabled);
|
|
185
|
+
await fsp.mkdir(path.dirname(configPath), { recursive: true });
|
|
186
|
+
await fsp.writeFile(configPath, next, 'utf8');
|
|
187
|
+
return next;
|
|
188
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AgentRuntimeManagementError,
|
|
3
|
+
} from '../../../agent-runtime/src/index';
|
|
4
|
+
|
|
5
|
+
export class CodexManagementError extends AgentRuntimeManagementError {
|
|
6
|
+
constructor(
|
|
7
|
+
statusCode: ConstructorParameters<typeof AgentRuntimeManagementError>[0],
|
|
8
|
+
payload: ConstructorParameters<typeof AgentRuntimeManagementError>[1],
|
|
9
|
+
) {
|
|
10
|
+
super(statusCode, payload);
|
|
11
|
+
this.name = 'CodexManagementError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function codexBadRequest(message: string): never {
|
|
16
|
+
throw new AgentRuntimeManagementError(400, {
|
|
17
|
+
code: 'bad_request',
|
|
18
|
+
message,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
buildTurnPricingSnapshot,
|
|
9
|
+
contextWindowForModel,
|
|
10
|
+
estimateTurnPrice,
|
|
11
|
+
resetPricingConfigCacheForTest,
|
|
12
|
+
supportsFastMode,
|
|
13
|
+
} from './modelPricing';
|
|
14
|
+
|
|
15
|
+
const sampleUsage = {
|
|
16
|
+
total: {
|
|
17
|
+
totalTokens: 3000,
|
|
18
|
+
inputTokens: 1500,
|
|
19
|
+
cachedInputTokens: 500,
|
|
20
|
+
outputTokens: 1500,
|
|
21
|
+
reasoningOutputTokens: 0,
|
|
22
|
+
},
|
|
23
|
+
last: {
|
|
24
|
+
totalTokens: 3000,
|
|
25
|
+
inputTokens: 1500,
|
|
26
|
+
cachedInputTokens: 500,
|
|
27
|
+
outputTokens: 1500,
|
|
28
|
+
reasoningOutputTokens: 0,
|
|
29
|
+
},
|
|
30
|
+
modelContextWindow: 272000,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe('modelPricing', () => {
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
resetPricingConfigCacheForTest();
|
|
36
|
+
vi.unstubAllEnvs();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('prices gpt-5.5 standard turns from the local pricing config', () => {
|
|
40
|
+
const estimate = estimateTurnPrice(sampleUsage, {
|
|
41
|
+
pricingModelKey: 'gpt-5.5',
|
|
42
|
+
pricingTierKey: 'standard',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(estimate).toMatchObject({
|
|
46
|
+
pricingModelKey: 'gpt-5.5',
|
|
47
|
+
pricingTierKey: 'standard',
|
|
48
|
+
inputUsd: 0.005,
|
|
49
|
+
cachedInputUsd: 0.00025,
|
|
50
|
+
outputUsd: 0.045,
|
|
51
|
+
});
|
|
52
|
+
expect(estimate?.totalUsd).toBeCloseTo(0.05025, 10);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('uses the gpt-5.5-specific fast multiplier and marks it fast-capable', () => {
|
|
56
|
+
expect(supportsFastMode('gpt-5.5')).toBe(true);
|
|
57
|
+
expect(contextWindowForModel('gpt-5.5')).toBe(272000);
|
|
58
|
+
|
|
59
|
+
const estimate = estimateTurnPrice(sampleUsage, {
|
|
60
|
+
pricingModelKey: 'gpt-5.5',
|
|
61
|
+
pricingTierKey: 'fast',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(estimate).toMatchObject({
|
|
65
|
+
pricingModelKey: 'gpt-5.5',
|
|
66
|
+
pricingTierKey: 'fast',
|
|
67
|
+
inputUsd: 0.0125,
|
|
68
|
+
cachedInputUsd: 0.000625,
|
|
69
|
+
outputUsd: 0.1125,
|
|
70
|
+
});
|
|
71
|
+
expect(estimate?.totalUsd).toBeCloseTo(0.125625, 10);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('prices Claude Sonnet and its 1M context option from the local pricing config', () => {
|
|
75
|
+
expect(supportsFastMode('sonnet')).toBe(false);
|
|
76
|
+
expect(contextWindowForModel('sonnet')).toBe(200000);
|
|
77
|
+
expect(contextWindowForModel('sonnet[1m]')).toBe(1000000);
|
|
78
|
+
|
|
79
|
+
const standardEstimate = estimateTurnPrice(sampleUsage, {
|
|
80
|
+
pricingModelKey: 'sonnet',
|
|
81
|
+
pricingTierKey: 'standard',
|
|
82
|
+
});
|
|
83
|
+
expect(standardEstimate).toMatchObject({
|
|
84
|
+
pricingModelKey: 'sonnet',
|
|
85
|
+
pricingTierKey: 'standard',
|
|
86
|
+
inputUsd: 0.003,
|
|
87
|
+
cachedInputUsd: 0.00015,
|
|
88
|
+
outputUsd: 0.0225,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const oneMillionEstimate = estimateTurnPrice(sampleUsage, {
|
|
92
|
+
pricingModelKey: 'sonnet[1m]',
|
|
93
|
+
pricingTierKey: 'standard',
|
|
94
|
+
});
|
|
95
|
+
expect(oneMillionEstimate).toMatchObject({
|
|
96
|
+
pricingModelKey: 'sonnet[1m]',
|
|
97
|
+
pricingTierKey: 'standard',
|
|
98
|
+
inputUsd: 0.006,
|
|
99
|
+
cachedInputUsd: 0.0003,
|
|
100
|
+
outputUsd: 0.03375,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('prices current Claude Opus and Haiku aliases from the local pricing config', () => {
|
|
105
|
+
expect(contextWindowForModel('claude-opus-4-7')).toBe(200000);
|
|
106
|
+
expect(contextWindowForModel('claude-haiku-4-5')).toBe(200000);
|
|
107
|
+
|
|
108
|
+
const opusEstimate = estimateTurnPrice(sampleUsage, {
|
|
109
|
+
pricingModelKey: 'claude-opus-4-7',
|
|
110
|
+
pricingTierKey: 'standard',
|
|
111
|
+
});
|
|
112
|
+
expect(opusEstimate).toMatchObject({
|
|
113
|
+
pricingModelKey: 'claude-opus-4-7',
|
|
114
|
+
pricingTierKey: 'standard',
|
|
115
|
+
inputUsd: 0.005,
|
|
116
|
+
cachedInputUsd: 0.00025,
|
|
117
|
+
outputUsd: 0.0375,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const haikuEstimate = estimateTurnPrice(sampleUsage, {
|
|
121
|
+
pricingModelKey: 'claude-haiku-4-5',
|
|
122
|
+
pricingTierKey: 'standard',
|
|
123
|
+
});
|
|
124
|
+
expect(haikuEstimate).toMatchObject({
|
|
125
|
+
pricingModelKey: 'claude-haiku-4-5',
|
|
126
|
+
pricingTierKey: 'standard',
|
|
127
|
+
inputUsd: 0.001,
|
|
128
|
+
cachedInputUsd: 0.00005,
|
|
129
|
+
outputUsd: 0.0075,
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('normalizes Claude date-stamped runtime model names to local pricing keys', () => {
|
|
134
|
+
expect(contextWindowForModel('claude-sonnet-4-5-20250929')).toBe(200000);
|
|
135
|
+
expect(supportsFastMode('claude-sonnet-4-5-20250929')).toBe(false);
|
|
136
|
+
expect(buildTurnPricingSnapshot('claude-sonnet-4-5-20250929', false)).toEqual({
|
|
137
|
+
pricingModelKey: 'claude-sonnet-4-5',
|
|
138
|
+
pricingTierKey: 'standard',
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const estimate = estimateTurnPrice(sampleUsage, {
|
|
142
|
+
pricingModelKey: 'claude-sonnet-4-5-20250929',
|
|
143
|
+
pricingTierKey: 'standard',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(estimate).toMatchObject({
|
|
147
|
+
pricingModelKey: 'claude-sonnet-4-5',
|
|
148
|
+
pricingTierKey: 'standard',
|
|
149
|
+
inputUsd: 0.003,
|
|
150
|
+
cachedInputUsd: 0.00015,
|
|
151
|
+
outputUsd: 0.0225,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('resolves pricing config from the installed package root when provided', async () => {
|
|
156
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'remote-codex-pricing-root-'));
|
|
157
|
+
await fs.mkdir(path.join(tempDir, 'config'), { recursive: true });
|
|
158
|
+
await fs.writeFile(
|
|
159
|
+
path.join(tempDir, 'config', 'codex-model-pricing.json'),
|
|
160
|
+
JSON.stringify({
|
|
161
|
+
currency: 'USD',
|
|
162
|
+
tiers: {
|
|
163
|
+
standard: { multiplier: 1 },
|
|
164
|
+
fast: { multiplier: 2 },
|
|
165
|
+
},
|
|
166
|
+
models: {
|
|
167
|
+
'package-model': {
|
|
168
|
+
inputUsdPerMillion: 10,
|
|
169
|
+
cachedInputUsdPerMillion: 1,
|
|
170
|
+
outputUsdPerMillion: 20,
|
|
171
|
+
supportsFastMode: true,
|
|
172
|
+
contextWindowTokens: 123000,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
}),
|
|
176
|
+
'utf8',
|
|
177
|
+
);
|
|
178
|
+
vi.stubEnv('REMOTE_CODEX_PACKAGE_ROOT', tempDir);
|
|
179
|
+
resetPricingConfigCacheForTest();
|
|
180
|
+
|
|
181
|
+
expect(contextWindowForModel('package-model')).toBe(123000);
|
|
182
|
+
expect(supportsFastMode('package-model')).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { AgentRuntimeError } from '../../agent-runtime/src/index';
|
|
4
|
+
import { JsonRpcClientError } from './jsonrpc';
|
|
5
|
+
import {
|
|
6
|
+
isCodexRuntimeRequestError,
|
|
7
|
+
isRemoteThreadBootstrapError,
|
|
8
|
+
isUnsupportedHooksListError,
|
|
9
|
+
parseTurnSteerRace,
|
|
10
|
+
unwrapCodexJsonRpcError,
|
|
11
|
+
} from './runtime-errors';
|
|
12
|
+
|
|
13
|
+
describe('codex runtime error helpers', () => {
|
|
14
|
+
it('unwraps Codex JSON-RPC errors from runtime errors', () => {
|
|
15
|
+
const cause = new JsonRpcClientError('method not found', 'remote_error', { code: -32601 });
|
|
16
|
+
const error = new AgentRuntimeError('method not found', 'codex', 'remote_error', {}, cause);
|
|
17
|
+
|
|
18
|
+
expect(unwrapCodexJsonRpcError(error)).toBe(cause);
|
|
19
|
+
expect(isCodexRuntimeRequestError(error)).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('classifies Codex remote errors used by ThreadService fallback paths', () => {
|
|
23
|
+
expect(
|
|
24
|
+
isRemoteThreadBootstrapError(
|
|
25
|
+
new AgentRuntimeError(
|
|
26
|
+
'failed to load rollout: rollout at /tmp/demo.jsonl is empty',
|
|
27
|
+
'codex',
|
|
28
|
+
'remote_error',
|
|
29
|
+
{},
|
|
30
|
+
new JsonRpcClientError(
|
|
31
|
+
'failed to load rollout: rollout at /tmp/demo.jsonl is empty',
|
|
32
|
+
'remote_error',
|
|
33
|
+
),
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
).toBe(true);
|
|
37
|
+
|
|
38
|
+
expect(
|
|
39
|
+
isUnsupportedHooksListError(
|
|
40
|
+
new AgentRuntimeError(
|
|
41
|
+
'endpoint not found: hooks/list',
|
|
42
|
+
'codex',
|
|
43
|
+
'remote_error',
|
|
44
|
+
{},
|
|
45
|
+
new JsonRpcClientError('endpoint not found: hooks/list', 'remote_error', {
|
|
46
|
+
code: -32601,
|
|
47
|
+
}),
|
|
48
|
+
),
|
|
49
|
+
),
|
|
50
|
+
).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('parses Codex steer races from wrapped runtime errors', () => {
|
|
54
|
+
expect(
|
|
55
|
+
parseTurnSteerRace(
|
|
56
|
+
new AgentRuntimeError(
|
|
57
|
+
'expected active turn id `turn-old` but found `turn-new`',
|
|
58
|
+
'codex',
|
|
59
|
+
'remote_error',
|
|
60
|
+
{},
|
|
61
|
+
new JsonRpcClientError(
|
|
62
|
+
'expected active turn id `turn-old` but found `turn-new`',
|
|
63
|
+
'remote_error',
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
).toEqual({
|
|
68
|
+
type: 'turnIdMismatch',
|
|
69
|
+
actualTurnId: 'turn-new',
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AgentRuntimeError,
|
|
3
|
+
isRemoteThreadBootstrapError,
|
|
4
|
+
isRuntimeRequestError,
|
|
5
|
+
isUnsupportedHooksListError,
|
|
6
|
+
parseTurnSteerRace,
|
|
7
|
+
TurnSteerRace,
|
|
8
|
+
} from '../../agent-runtime/src/index';
|
|
9
|
+
import { JsonRpcClientError } from './jsonrpc';
|
|
10
|
+
|
|
11
|
+
export type CodexTurnSteerRace = TurnSteerRace;
|
|
12
|
+
export {
|
|
13
|
+
isRemoteThreadBootstrapError,
|
|
14
|
+
isUnsupportedHooksListError,
|
|
15
|
+
parseTurnSteerRace,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function unwrapCodexJsonRpcError(error: unknown): JsonRpcClientError | null {
|
|
19
|
+
if (error instanceof JsonRpcClientError) {
|
|
20
|
+
return error;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (error instanceof AgentRuntimeError && error.provider === 'codex') {
|
|
24
|
+
return error.cause instanceof JsonRpcClientError ? error.cause : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isCodexRemoteError(error: unknown) {
|
|
31
|
+
const codexError = unwrapCodexJsonRpcError(error);
|
|
32
|
+
return codexError?.code === 'remote_error' ? codexError : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isCodexRuntimeRequestError(error: unknown) {
|
|
36
|
+
return Boolean(unwrapCodexJsonRpcError(error)) || isRuntimeRequestError(error);
|
|
37
|
+
}
|
|
@@ -23,6 +23,9 @@ import type {
|
|
|
23
23
|
StartAgentSessionResult,
|
|
24
24
|
StartAgentTurnInput,
|
|
25
25
|
} from '../../agent-runtime/src/index';
|
|
26
|
+
import type {
|
|
27
|
+
AgentBackendInstallationDto,
|
|
28
|
+
} from '../../shared/src/index';
|
|
26
29
|
import {
|
|
27
30
|
buildCodexProviderRequestResponse,
|
|
28
31
|
mapCodexProviderRequest,
|
|
@@ -51,6 +54,7 @@ import {
|
|
|
51
54
|
ThreadStartInput,
|
|
52
55
|
TurnStartInput,
|
|
53
56
|
JsonRpcClientError,
|
|
57
|
+
supportsFastMode,
|
|
54
58
|
} from './index';
|
|
55
59
|
|
|
56
60
|
export const codexCapabilities: AgentProviderCapabilities = {
|
|
@@ -137,6 +141,7 @@ function mapModel(model: Awaited<ReturnType<CodexAppServerManager['listModels']>
|
|
|
137
141
|
description: model.description,
|
|
138
142
|
isDefault: model.isDefault,
|
|
139
143
|
hidden: model.hidden,
|
|
144
|
+
supportsPerformanceMode: supportsFastMode(model.model),
|
|
140
145
|
supportedReasoningEfforts: model.supportedReasoningEfforts.map((entry) => ({
|
|
141
146
|
reasoningEffort: entry.reasoningEffort,
|
|
142
147
|
description: entry.description,
|
|
@@ -209,6 +214,7 @@ function buildSandboxPolicy(
|
|
|
209
214
|
}
|
|
210
215
|
|
|
211
216
|
function mapSession(thread: CodexThreadRecord): AgentSessionDetail {
|
|
217
|
+
const threadWithTotal = thread as CodexThreadRecord & { totalTurnCount?: unknown };
|
|
212
218
|
return {
|
|
213
219
|
provider: 'codex',
|
|
214
220
|
providerSessionId: thread.id,
|
|
@@ -219,6 +225,10 @@ function mapSession(thread: CodexThreadRecord): AgentSessionDetail {
|
|
|
219
225
|
updatedAt: toIsoFromEpoch(thread.updatedAt),
|
|
220
226
|
status: normalizeStatus(thread.status),
|
|
221
227
|
turns: thread.turns.map(mapTurn),
|
|
228
|
+
totalTurnCount:
|
|
229
|
+
typeof threadWithTotal.totalTurnCount === 'number'
|
|
230
|
+
? threadWithTotal.totalTurnCount
|
|
231
|
+
: null,
|
|
222
232
|
rawSession: thread,
|
|
223
233
|
};
|
|
224
234
|
}
|
|
@@ -460,6 +470,16 @@ export class CodexRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
460
470
|
readonly displayName = 'Codex';
|
|
461
471
|
readonly description = 'Local Codex app-server runtime.';
|
|
462
472
|
readonly capabilities = codexCapabilities;
|
|
473
|
+
readonly installation: AgentBackendInstallationDto = {
|
|
474
|
+
packageName: '@openai/codex',
|
|
475
|
+
installed: true,
|
|
476
|
+
installedVersion: null,
|
|
477
|
+
latestVersion: null,
|
|
478
|
+
installCommand: null,
|
|
479
|
+
updateCommand: 'npm install -g @openai/codex@latest',
|
|
480
|
+
busy: false,
|
|
481
|
+
lastError: null,
|
|
482
|
+
};
|
|
463
483
|
readonly managementSchema: AgentRuntimeManagementSchema = {
|
|
464
484
|
hostConfigFiles: [
|
|
465
485
|
{
|
|
@@ -573,8 +593,11 @@ export class CodexRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
573
593
|
return codexRuntimeCall(() => this.manager.listLoadedThreads());
|
|
574
594
|
}
|
|
575
595
|
|
|
576
|
-
async readSession(
|
|
577
|
-
|
|
596
|
+
async readSession(
|
|
597
|
+
providerSessionId: string,
|
|
598
|
+
options: { limit?: number; beforeTurnId?: string | null } = {},
|
|
599
|
+
): Promise<AgentSessionDetail> {
|
|
600
|
+
return mapSession(await codexRuntimeCall(() => this.manager.readThread(providerSessionId, options)));
|
|
578
601
|
}
|
|
579
602
|
|
|
580
603
|
async startSession(input: StartAgentSessionInput): Promise<StartAgentSessionResult> {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { truncateAutoThreadTitle } from '../../shared/src/index';
|
|
@@ -416,6 +416,21 @@ export function deleteThreadTurnMetadataByThreadId(db: DatabaseClient, threadId:
|
|
|
416
416
|
db.delete(threadTurnMetadata).where(eq(threadTurnMetadata.threadId, threadId)).run();
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
+
export function deleteThreadTurnMetadataByThreadAndTurnId(
|
|
420
|
+
db: DatabaseClient,
|
|
421
|
+
threadId: string,
|
|
422
|
+
turnId: string,
|
|
423
|
+
) {
|
|
424
|
+
db.delete(threadTurnMetadata)
|
|
425
|
+
.where(
|
|
426
|
+
and(
|
|
427
|
+
eq(threadTurnMetadata.threadId, threadId),
|
|
428
|
+
eq(threadTurnMetadata.turnId, turnId),
|
|
429
|
+
),
|
|
430
|
+
)
|
|
431
|
+
.run();
|
|
432
|
+
}
|
|
433
|
+
|
|
419
434
|
export function listThreadHistoryItemRecordsByThreadId(
|
|
420
435
|
db: DatabaseClient,
|
|
421
436
|
threadId: string,
|
|
@@ -476,6 +491,21 @@ export function deleteThreadHistoryItemRecordsByThreadId(
|
|
|
476
491
|
db.delete(threadHistoryItems).where(eq(threadHistoryItems.threadId, threadId)).run();
|
|
477
492
|
}
|
|
478
493
|
|
|
494
|
+
export function deleteThreadHistoryItemRecordsByThreadAndTurnId(
|
|
495
|
+
db: DatabaseClient,
|
|
496
|
+
threadId: string,
|
|
497
|
+
turnId: string,
|
|
498
|
+
) {
|
|
499
|
+
db.delete(threadHistoryItems)
|
|
500
|
+
.where(
|
|
501
|
+
and(
|
|
502
|
+
eq(threadHistoryItems.threadId, threadId),
|
|
503
|
+
eq(threadHistoryItems.turnId, turnId),
|
|
504
|
+
),
|
|
505
|
+
)
|
|
506
|
+
.run();
|
|
507
|
+
}
|
|
508
|
+
|
|
479
509
|
export function listThreadPendingSteerRecordsByThreadId(
|
|
480
510
|
db: DatabaseClient,
|
|
481
511
|
threadId: string,
|