@nyssance/codex-acp-v2 0.4.0 → 0.5.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 +22 -0
- package/dist/index.js +289 -76
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 — 2026-09-11
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `providers/set` hints `_meta.codex.bearerToken` (written as `experimental_bearer_token`)
|
|
8
|
+
and `_meta.codex.config` (top-level Codex thread-config overrides that ride with the
|
|
9
|
+
gateway; a `model_catalog_json` file is read so gateway models carry their real reasoning
|
|
10
|
+
levels and modalities). `providers/disable` removes both from live threads.
|
|
11
|
+
- Catalog mode (`_meta.codex.mode: "catalog"`): the gateway's models are offered next to
|
|
12
|
+
Codex's in every session's `model` option as their own select group; selecting one moves
|
|
13
|
+
only that session to the gateway (unsubscribe + resume, materializing an empty thread
|
|
14
|
+
first), selecting a Codex model moves it back. `session/new` / `session/fork` honour
|
|
15
|
+
`_meta.alwith.model`; `session/resume` follows the provider Codex recorded for the thread.
|
|
16
|
+
Advertised as `capabilities._meta.codex.providerCatalog`.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- A thread another Codex client is writing ("already has an active writer", a file lock
|
|
21
|
+
across the Codex home) opens as a read-only session through `thread/read`, like Codex's
|
|
22
|
+
TUI: history replays, the response carries `_meta.codex.readOnly`, prompts and config
|
|
23
|
+
changes are refused with `-32600`, and a later resume retries the real thing.
|
|
24
|
+
|
|
3
25
|
## 0.4.0 — 2026-09-10
|
|
4
26
|
|
|
5
27
|
### Added
|
package/dist/index.js
CHANGED
|
@@ -3406,8 +3406,8 @@ var require_main = __commonJS(function(exports) {
|
|
|
3406
3406
|
exports.createMessageConnection = createMessageConnection;
|
|
3407
3407
|
var ril_1 = __importDefault(require_ril());
|
|
3408
3408
|
ril_1.default.install();
|
|
3409
|
-
var
|
|
3410
|
-
var
|
|
3409
|
+
var path8 = __importStar(__require("path"));
|
|
3410
|
+
var os3 = __importStar(__require("os"));
|
|
3411
3411
|
var fs7 = __importStar(__require("fs"));
|
|
3412
3412
|
var crypto_1 = __require("crypto");
|
|
3413
3413
|
var net_1 = __require("net");
|
|
@@ -3555,7 +3555,7 @@ var require_main = __commonJS(function(exports) {
|
|
|
3555
3555
|
}
|
|
3556
3556
|
let randomLength = 32;
|
|
3557
3557
|
const fixedLength = "/lsp-.sock".length;
|
|
3558
|
-
const tmpDir = fs7.realpathSync(XDG_RUNTIME_DIR ??
|
|
3558
|
+
const tmpDir = fs7.realpathSync(XDG_RUNTIME_DIR ?? os3.tmpdir());
|
|
3559
3559
|
const limit = safeIpcPathLengths.get(process.platform);
|
|
3560
3560
|
if (limit !== undefined) {
|
|
3561
3561
|
randomLength = Math.min(limit - tmpDir.length - fixedLength, randomLength);
|
|
@@ -3564,7 +3564,7 @@ var require_main = __commonJS(function(exports) {
|
|
|
3564
3564
|
throw new Error(`Unable to generate a random pipe name with ${randomLength} characters.`);
|
|
3565
3565
|
}
|
|
3566
3566
|
const randomSuffix = (0, crypto_1.randomBytes)(Math.floor(randomLength / 2)).toString("hex");
|
|
3567
|
-
return
|
|
3567
|
+
return path8.join(tmpDir, `lsp-${randomSuffix}.sock`);
|
|
3568
3568
|
}
|
|
3569
3569
|
function createClientPipeTransport(pipeName, encoding = "utf-8") {
|
|
3570
3570
|
let connectResolve;
|
|
@@ -21721,7 +21721,7 @@ var package_default = {
|
|
|
21721
21721
|
publishConfig: {
|
|
21722
21722
|
access: "public"
|
|
21723
21723
|
},
|
|
21724
|
-
version: "0.
|
|
21724
|
+
version: "0.5.0",
|
|
21725
21725
|
description: "Native ACP v2 agent for the OpenAI Codex app-server",
|
|
21726
21726
|
packageManager: "bun@1.4.0",
|
|
21727
21727
|
engines: {
|
|
@@ -21838,7 +21838,7 @@ function classifyTurnError(info) {
|
|
|
21838
21838
|
}
|
|
21839
21839
|
|
|
21840
21840
|
// src/agent/CodexAgent.ts
|
|
21841
|
-
import
|
|
21841
|
+
import path7 from "path";
|
|
21842
21842
|
|
|
21843
21843
|
// src/util/logger.ts
|
|
21844
21844
|
import fs from "fs";
|
|
@@ -23982,7 +23982,7 @@ function modelLacksReasoning(model) {
|
|
|
23982
23982
|
const options = supportedEfforts(model);
|
|
23983
23983
|
return options.length > 0 && options.every((option) => option.reasoningEffort === "none");
|
|
23984
23984
|
}
|
|
23985
|
-
function modelConfigOption(catalog, current) {
|
|
23985
|
+
function modelConfigOption(catalog, current, gatewayGroup = null) {
|
|
23986
23986
|
const visible = catalog.filter((model) => !model.hidden || model.id === current);
|
|
23987
23987
|
const options = visible.map((model) => ({
|
|
23988
23988
|
value: model.id,
|
|
@@ -23992,14 +23992,25 @@ function modelConfigOption(catalog, current) {
|
|
|
23992
23992
|
if (!options.some((option) => option.value === current)) {
|
|
23993
23993
|
options.unshift({ value: current, name: current, description: null });
|
|
23994
23994
|
}
|
|
23995
|
-
|
|
23995
|
+
const base = {
|
|
23996
23996
|
configId: MODEL_CONFIG_ID,
|
|
23997
23997
|
name: "Model",
|
|
23998
23998
|
description: "Model Codex uses for this session",
|
|
23999
23999
|
category: "model",
|
|
24000
24000
|
type: "select",
|
|
24001
|
-
currentValue: current
|
|
24002
|
-
|
|
24001
|
+
currentValue: current
|
|
24002
|
+
};
|
|
24003
|
+
if (gatewayGroup === null)
|
|
24004
|
+
return { ...base, options };
|
|
24005
|
+
const gatewayIds = new Set(gatewayGroup.modelIds);
|
|
24006
|
+
const native = options.filter((option) => !gatewayIds.has(option.value));
|
|
24007
|
+
const gateway = options.filter((option) => gatewayIds.has(option.value));
|
|
24008
|
+
return {
|
|
24009
|
+
...base,
|
|
24010
|
+
options: [
|
|
24011
|
+
{ groupId: "codex", name: "Codex", options: native },
|
|
24012
|
+
{ groupId: "custom-gateway", name: gatewayGroup.name, options: gateway }
|
|
24013
|
+
]
|
|
24003
24014
|
};
|
|
24004
24015
|
}
|
|
24005
24016
|
function effortConfigOption(efforts, current) {
|
|
@@ -25955,7 +25966,7 @@ function sessionConfigOptions(session) {
|
|
|
25955
25966
|
const model = findModel(session.catalog, session.model.model);
|
|
25956
25967
|
const options = [
|
|
25957
25968
|
modeConfigOption(session.mode),
|
|
25958
|
-
modelConfigOption(session.catalog, session.model.model),
|
|
25969
|
+
modelConfigOption(session.catalog, session.model.model, session.gatewayGroup),
|
|
25959
25970
|
effortConfigOption(supportedEfforts(model), session.model.effort),
|
|
25960
25971
|
collaborationModeConfigOption(session.collaborationMode)
|
|
25961
25972
|
];
|
|
@@ -26057,10 +26068,12 @@ function firstLine(text2) {
|
|
|
26057
26068
|
}
|
|
26058
26069
|
|
|
26059
26070
|
// src/agent/providers.ts
|
|
26071
|
+
import { readFileSync } from "fs";
|
|
26072
|
+
import os2 from "os";
|
|
26073
|
+
import path6 from "path";
|
|
26060
26074
|
var OPENAI_PROVIDER_ID = "openai";
|
|
26061
26075
|
var GATEWAY_MODEL_PROVIDER = "custom-gateway";
|
|
26062
26076
|
var DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
26063
|
-
|
|
26064
26077
|
class ProviderRouting {
|
|
26065
26078
|
baseConfig;
|
|
26066
26079
|
configuredProvider;
|
|
@@ -26078,12 +26091,14 @@ class ProviderRouting {
|
|
|
26078
26091
|
return this.gateway;
|
|
26079
26092
|
}
|
|
26080
26093
|
list() {
|
|
26094
|
+
const routed = this.gateway !== null && this.gateway.mode === "route";
|
|
26081
26095
|
return {
|
|
26082
26096
|
providers: [{
|
|
26083
26097
|
providerId: OPENAI_PROVIDER_ID,
|
|
26084
26098
|
supported: ["openai"],
|
|
26085
26099
|
required: false,
|
|
26086
|
-
current: this.gateway ? { apiType: "openai", baseUrl: this.gateway.baseUrl } : { apiType: "openai", baseUrl: this.nativeBaseUrl() }
|
|
26100
|
+
current: routed && this.gateway ? { apiType: "openai", baseUrl: this.gateway.baseUrl } : { apiType: "openai", baseUrl: this.nativeBaseUrl() },
|
|
26101
|
+
...this.gateway && !routed ? { _meta: { codex: { mode: "catalog", gateway: { name: this.gateway.name, baseUrl: this.gateway.baseUrl, models: this.gateway.models.map((model) => model.id) } } } } : {}
|
|
26087
26102
|
}]
|
|
26088
26103
|
};
|
|
26089
26104
|
}
|
|
@@ -26104,37 +26119,83 @@ class ProviderRouting {
|
|
|
26104
26119
|
headers: { ...request.headers ?? {} },
|
|
26105
26120
|
name: hints.name ?? "Client-configured gateway",
|
|
26106
26121
|
models: hints.models,
|
|
26107
|
-
model: hints.model
|
|
26122
|
+
model: hints.model,
|
|
26123
|
+
bearerToken: hints.bearerToken,
|
|
26124
|
+
config: hints.config,
|
|
26125
|
+
mode: hints.mode,
|
|
26126
|
+
catalogEntries: hints.catalogEntries
|
|
26108
26127
|
};
|
|
26109
26128
|
}
|
|
26129
|
+
get mode() {
|
|
26130
|
+
return this.gateway?.mode ?? "route";
|
|
26131
|
+
}
|
|
26132
|
+
isGatewayModel(modelId) {
|
|
26133
|
+
return this.gateway !== null && this.gateway.models.some((model) => model.id === modelId);
|
|
26134
|
+
}
|
|
26135
|
+
gatewayGroup() {
|
|
26136
|
+
if (!this.gateway || this.gateway.mode !== "catalog" || this.gateway.models.length === 0)
|
|
26137
|
+
return null;
|
|
26138
|
+
return { name: this.gateway.name, modelIds: this.gateway.models.map((model) => model.id) };
|
|
26139
|
+
}
|
|
26110
26140
|
disable(request) {
|
|
26111
26141
|
if (request.providerId === OPENAI_PROVIDER_ID)
|
|
26112
26142
|
this.gateway = null;
|
|
26113
26143
|
}
|
|
26114
26144
|
modelProvider() {
|
|
26115
|
-
return this.gateway ? GATEWAY_MODEL_PROVIDER : this.configuredProvider;
|
|
26145
|
+
return this.gateway && this.gateway.mode === "route" ? GATEWAY_MODEL_PROVIDER : this.configuredProvider;
|
|
26146
|
+
}
|
|
26147
|
+
routesByDefault() {
|
|
26148
|
+
return this.gateway !== null && this.gateway.mode === "route";
|
|
26116
26149
|
}
|
|
26117
|
-
threadConfig() {
|
|
26118
|
-
if (!this.gateway)
|
|
26150
|
+
threadConfig(onGateway = this.routesByDefault()) {
|
|
26151
|
+
if (!this.gateway || !onGateway)
|
|
26119
26152
|
return this.baseConfig;
|
|
26120
|
-
|
|
26153
|
+
return this.applyGateway(this.baseConfig, this.gateway);
|
|
26154
|
+
}
|
|
26155
|
+
routeConfig(config2, onGateway) {
|
|
26156
|
+
return this.rebind(config2, this, onGateway);
|
|
26157
|
+
}
|
|
26158
|
+
overrideKeys() {
|
|
26159
|
+
return this.gateway ? Object.keys(this.gateway.config) : [];
|
|
26160
|
+
}
|
|
26161
|
+
rebind(config2, previous, onGateway = this.routesByDefault()) {
|
|
26162
|
+
const next = { ...config2 };
|
|
26163
|
+
for (const key of previous.overrideKeys()) {
|
|
26164
|
+
if (key in this.baseConfig)
|
|
26165
|
+
next[key] = this.baseConfig[key];
|
|
26166
|
+
else
|
|
26167
|
+
delete next[key];
|
|
26168
|
+
}
|
|
26169
|
+
if (!this.gateway || !onGateway) {
|
|
26170
|
+
delete next["model_providers"];
|
|
26171
|
+
if (isRecord2(this.baseConfig["model_providers"]))
|
|
26172
|
+
next["model_providers"] = this.baseConfig["model_providers"];
|
|
26173
|
+
return next;
|
|
26174
|
+
}
|
|
26175
|
+
return this.applyGateway(next, this.gateway);
|
|
26176
|
+
}
|
|
26177
|
+
applyGateway(config2, gateway) {
|
|
26178
|
+
const existing = isRecord2(config2["model_providers"]) ? config2["model_providers"] : {};
|
|
26179
|
+
const entry = {
|
|
26180
|
+
name: gateway.name,
|
|
26181
|
+
base_url: gateway.baseUrl,
|
|
26182
|
+
wire_api: "responses",
|
|
26183
|
+
http_headers: { "X-Client-Feature-ID": "codex", ...gateway.headers }
|
|
26184
|
+
};
|
|
26185
|
+
if (gateway.bearerToken !== null)
|
|
26186
|
+
entry["experimental_bearer_token"] = gateway.bearerToken;
|
|
26121
26187
|
return {
|
|
26122
|
-
...
|
|
26123
|
-
|
|
26124
|
-
|
|
26125
|
-
[GATEWAY_MODEL_PROVIDER]: {
|
|
26126
|
-
name: this.gateway.name,
|
|
26127
|
-
base_url: this.gateway.baseUrl,
|
|
26128
|
-
wire_api: "responses",
|
|
26129
|
-
http_headers: { "X-Client-Feature-ID": "codex", ...this.gateway.headers }
|
|
26130
|
-
}
|
|
26131
|
-
}
|
|
26188
|
+
...config2,
|
|
26189
|
+
...gateway.config,
|
|
26190
|
+
model_providers: { ...existing, [GATEWAY_MODEL_PROVIDER]: entry }
|
|
26132
26191
|
};
|
|
26133
26192
|
}
|
|
26134
26193
|
catalog(codexCatalog) {
|
|
26135
26194
|
if (!this.gateway || this.gateway.models.length === 0)
|
|
26136
26195
|
return codexCatalog;
|
|
26137
|
-
|
|
26196
|
+
const gateway = this.gateway;
|
|
26197
|
+
const own = gateway.models.map((model, index) => gatewayModel(model, gateway.mode === "route" && index === 0, codexCatalog[0], gateway.catalogEntries.get(model.id)));
|
|
26198
|
+
return gateway.mode === "catalog" ? [...codexCatalog, ...own] : own;
|
|
26138
26199
|
}
|
|
26139
26200
|
nativeBaseUrl() {
|
|
26140
26201
|
const providers = this.baseConfig["model_providers"];
|
|
@@ -26165,22 +26226,80 @@ function readHints(meta) {
|
|
|
26165
26226
|
}
|
|
26166
26227
|
const model = typeof alwith["model"] === "string" && alwith["model"].length > 0 ? alwith["model"] : null;
|
|
26167
26228
|
const name = typeof codex["name"] === "string" && codex["name"].length > 0 ? codex["name"] : null;
|
|
26168
|
-
|
|
26229
|
+
const bearerToken = typeof codex["bearerToken"] === "string" && codex["bearerToken"].length > 0 ? codex["bearerToken"] : null;
|
|
26230
|
+
const rawConfig = codex["config"];
|
|
26231
|
+
if (rawConfig !== undefined && !isRecord2(rawConfig)) {
|
|
26232
|
+
throw RequestError.invalidParams({ config: rawConfig }, "_meta.codex.config must be an object of top-level Codex config keys");
|
|
26233
|
+
}
|
|
26234
|
+
const config2 = {};
|
|
26235
|
+
if (isRecord2(rawConfig)) {
|
|
26236
|
+
for (const [key, value] of Object.entries(rawConfig)) {
|
|
26237
|
+
if (key === "model_providers")
|
|
26238
|
+
throw RequestError.invalidParams({ config: rawConfig }, "_meta.codex.config cannot set model_providers; that entry is derived from baseUrl and headers");
|
|
26239
|
+
config2[key] = value;
|
|
26240
|
+
}
|
|
26241
|
+
}
|
|
26242
|
+
const rawMode = codex["mode"];
|
|
26243
|
+
if (rawMode !== undefined && rawMode !== "route" && rawMode !== "catalog") {
|
|
26244
|
+
throw RequestError.invalidParams({ mode: rawMode }, '_meta.codex.mode must be "route" or "catalog"');
|
|
26245
|
+
}
|
|
26246
|
+
const mode = rawMode === "catalog" ? "catalog" : "route";
|
|
26247
|
+
if (mode === "catalog" && models.length === 0) {
|
|
26248
|
+
throw RequestError.invalidParams(undefined, `_meta.codex.mode "catalog" needs _meta.alwith.models: the models offered next to Codex's`);
|
|
26249
|
+
}
|
|
26250
|
+
const catalogPath = config2["model_catalog_json"];
|
|
26251
|
+
const catalogEntries = typeof catalogPath === "string" ? readCatalogFile(catalogPath) : new Map;
|
|
26252
|
+
return { models, model, name, bearerToken, config: config2, mode, catalogEntries };
|
|
26253
|
+
}
|
|
26254
|
+
function readCatalogFile(catalogPath) {
|
|
26255
|
+
const resolved = catalogPath.startsWith("~/") ? path6.join(os2.homedir(), catalogPath.slice(2)) : catalogPath;
|
|
26256
|
+
let parsed;
|
|
26257
|
+
try {
|
|
26258
|
+
parsed = JSON.parse(readFileSync(resolved, "utf8"));
|
|
26259
|
+
} catch (error40) {
|
|
26260
|
+
throw RequestError.invalidParams({ model_catalog_json: catalogPath }, `model_catalog_json could not be read as JSON: ${error40 instanceof Error ? error40.message : String(error40)}`);
|
|
26261
|
+
}
|
|
26262
|
+
const models = isRecord2(parsed) && Array.isArray(parsed["models"]) ? parsed["models"] : null;
|
|
26263
|
+
if (models === null)
|
|
26264
|
+
throw RequestError.invalidParams({ model_catalog_json: catalogPath }, "model_catalog_json must be {models: [...]} as Codex expects");
|
|
26265
|
+
const entries = new Map;
|
|
26266
|
+
for (const entry of models) {
|
|
26267
|
+
if (!isRecord2(entry) || typeof entry["slug"] !== "string")
|
|
26268
|
+
continue;
|
|
26269
|
+
const levels = Array.isArray(entry["supported_reasoning_levels"]) ? entry["supported_reasoning_levels"] : [];
|
|
26270
|
+
const efforts = [];
|
|
26271
|
+
for (const level of levels) {
|
|
26272
|
+
if (!isRecord2(level) || typeof level["effort"] !== "string")
|
|
26273
|
+
continue;
|
|
26274
|
+
efforts.push({ reasoningEffort: level["effort"], description: typeof level["description"] === "string" ? level["description"] : "" });
|
|
26275
|
+
}
|
|
26276
|
+
const modalities = Array.isArray(entry["input_modalities"]) ? entry["input_modalities"].filter((value) => value === "text" || value === "image") : null;
|
|
26277
|
+
entries.set(entry["slug"], {
|
|
26278
|
+
displayName: typeof entry["display_name"] === "string" ? entry["display_name"] : null,
|
|
26279
|
+
description: typeof entry["description"] === "string" ? entry["description"] : null,
|
|
26280
|
+
efforts,
|
|
26281
|
+
defaultEffort: typeof entry["default_reasoning_level"] === "string" ? entry["default_reasoning_level"] : null,
|
|
26282
|
+
inputModalities: modalities
|
|
26283
|
+
});
|
|
26284
|
+
}
|
|
26285
|
+
return entries;
|
|
26169
26286
|
}
|
|
26170
|
-
function gatewayModel(model, isDefault, template) {
|
|
26287
|
+
function gatewayModel(model, isDefault, template, entry) {
|
|
26288
|
+
const efforts = entry && entry.efforts.length > 0 ? entry.efforts : template?.supportedReasoningEfforts ?? [];
|
|
26289
|
+
const defaultEffort = entry?.defaultEffort ?? template?.defaultReasoningEffort ?? "medium";
|
|
26171
26290
|
return {
|
|
26172
26291
|
id: model.id,
|
|
26173
26292
|
model: model.id,
|
|
26174
26293
|
upgrade: null,
|
|
26175
26294
|
upgradeInfo: null,
|
|
26176
26295
|
availabilityNux: null,
|
|
26177
|
-
displayName: model.name,
|
|
26178
|
-
description: model.description ?? "",
|
|
26296
|
+
displayName: model.name !== model.id ? model.name : entry?.displayName ?? model.name,
|
|
26297
|
+
description: model.description ?? entry?.description ?? "",
|
|
26179
26298
|
modelSpecialty: null,
|
|
26180
26299
|
hidden: false,
|
|
26181
|
-
supportedReasoningEfforts:
|
|
26182
|
-
defaultReasoningEffort:
|
|
26183
|
-
inputModalities: ["text", "image"],
|
|
26300
|
+
supportedReasoningEfforts: efforts,
|
|
26301
|
+
defaultReasoningEffort: defaultEffort,
|
|
26302
|
+
inputModalities: entry?.inputModalities ?? ["text", "image"],
|
|
26184
26303
|
supportsPersonality: false,
|
|
26185
26304
|
multiAgentVersion: null,
|
|
26186
26305
|
additionalSpeedTiers: [],
|
|
@@ -26323,7 +26442,7 @@ class CodexAgent {
|
|
|
26323
26442
|
additionalDirectories: {}
|
|
26324
26443
|
},
|
|
26325
26444
|
providers: {},
|
|
26326
|
-
_meta: { codex: { archive: true, seedHistory: true, skills: true, plugins: true } }
|
|
26445
|
+
_meta: { codex: { archive: true, seedHistory: true, skills: true, plugins: true, providerCatalog: true } }
|
|
26327
26446
|
},
|
|
26328
26447
|
authMethods: authMethods(this.capabilities, this.env)
|
|
26329
26448
|
};
|
|
@@ -26408,23 +26527,27 @@ class CodexAgent {
|
|
|
26408
26527
|
try {
|
|
26409
26528
|
change(candidate);
|
|
26410
26529
|
const catalog = await this.withCodex(() => this.codex.allModels(true));
|
|
26411
|
-
const
|
|
26412
|
-
|
|
26530
|
+
const nativeProvider = await this.resolveModelProvider(candidate);
|
|
26531
|
+
const wholesale = this.providers.active !== null && this.providers.mode === "route" || candidate.active !== null && candidate.mode === "route";
|
|
26532
|
+
const moving = [...this.sessions.values()].filter((runtime) => wholesale || runtime.modelProvider === GATEWAY_MODEL_PROVIDER);
|
|
26533
|
+
const targets = new Map(moving.map((runtime) => [runtime, candidate.active !== null && (candidate.mode === "route" || runtime.modelProvider === GATEWAY_MODEL_PROVIDER)]));
|
|
26534
|
+
for (const runtime of moving) {
|
|
26413
26535
|
try {
|
|
26414
26536
|
await this.codex.threadTurnsList({ threadId: runtime.session.id, limit: 1, itemsView: "notLoaded" });
|
|
26415
26537
|
} catch (error40) {
|
|
26416
|
-
|
|
26538
|
+
if (wholesale) {
|
|
26539
|
+
throw RequestError.invalidRequest({ sessionId: runtime.session.id, details: errorMessage(error40) }, "Cannot reload this session's history for a provider change; close empty sessions and configure the provider before creating them, or retry after history is available");
|
|
26540
|
+
}
|
|
26541
|
+
await this.withCodex(() => this.codex.threadInjectItems({ threadId: runtime.session.id, items: [providerChangeNote()] }));
|
|
26417
26542
|
}
|
|
26418
26543
|
}
|
|
26419
|
-
|
|
26420
|
-
|
|
26421
|
-
const
|
|
26422
|
-
|
|
26423
|
-
|
|
26424
|
-
|
|
26425
|
-
|
|
26426
|
-
const nextCatalog = candidate.catalog(catalog);
|
|
26427
|
-
const requestedModel = candidate.active?.model ?? (findModel(nextCatalog, runtime.session.model.model) ? runtime.session.model.model : null);
|
|
26544
|
+
const nextCatalog = candidate.catalog(catalog);
|
|
26545
|
+
for (const runtime of moving) {
|
|
26546
|
+
const onGateway = targets.get(runtime) === true;
|
|
26547
|
+
const config2 = candidate.rebind(runtime.config, this.providers, onGateway);
|
|
26548
|
+
const modelProvider = onGateway ? GATEWAY_MODEL_PROVIDER : nativeProvider;
|
|
26549
|
+
const keep = findModel(nextCatalog, runtime.session.model.model) && candidate.isGatewayModel(runtime.session.model.model) === onGateway;
|
|
26550
|
+
const requestedModel = candidate.mode === "route" ? candidate.active?.model ?? (keep ? runtime.session.model.model : null) : keep ? runtime.session.model.model : null;
|
|
26428
26551
|
const selection = resolveModelSelection(nextCatalog, requestedModel, requestedModel === runtime.session.model.model ? runtime.session.model.effort : null);
|
|
26429
26552
|
attempted.push(runtime);
|
|
26430
26553
|
await this.codex.threadUnsubscribe({ threadId: runtime.session.id });
|
|
@@ -26436,15 +26559,21 @@ class CodexAgent {
|
|
|
26436
26559
|
model: selection.model,
|
|
26437
26560
|
excludeTurns: true
|
|
26438
26561
|
}));
|
|
26439
|
-
staged.set(runtime, { config: config2, catalog: nextCatalog, model: resolveModelSelection(nextCatalog,
|
|
26562
|
+
staged.set(runtime, { config: config2, catalog: nextCatalog, model: resolveModelSelection(nextCatalog, requestedModel ?? thread.model, thread.reasoningEffort) });
|
|
26440
26563
|
}
|
|
26441
26564
|
this.providers = candidate;
|
|
26442
|
-
for (const
|
|
26443
|
-
|
|
26444
|
-
|
|
26445
|
-
|
|
26446
|
-
|
|
26447
|
-
|
|
26565
|
+
for (const runtime of this.sessions.values()) {
|
|
26566
|
+
const next = staged.get(runtime);
|
|
26567
|
+
if (next) {
|
|
26568
|
+
runtime.config = next.config;
|
|
26569
|
+
runtime.modelProvider = targets.get(runtime) === true ? GATEWAY_MODEL_PROVIDER : nativeProvider;
|
|
26570
|
+
runtime.session.model = next.model;
|
|
26571
|
+
runtime.stale = false;
|
|
26572
|
+
}
|
|
26573
|
+
runtime.session.catalog = nextCatalog;
|
|
26574
|
+
runtime.session.gatewayGroup = candidate.gatewayGroup();
|
|
26575
|
+
if (!next)
|
|
26576
|
+
runtime.session.model = resolveModelSelection(nextCatalog, findModel(nextCatalog, runtime.session.model.model) ? runtime.session.model.model : null, runtime.session.model.effort);
|
|
26448
26577
|
runtime.client.update({ sessionUpdate: "config_option_update", configOptions: sessionConfigOptions(runtime.session), _meta: { codex: { routing: { stale: false } } } }).catch((error40) => logger.error("Provider config notification failed", error40));
|
|
26449
26578
|
}
|
|
26450
26579
|
} catch (error40) {
|
|
@@ -26483,7 +26612,8 @@ class CodexAgent {
|
|
|
26483
26612
|
}
|
|
26484
26613
|
async resumeSession(params, signal) {
|
|
26485
26614
|
const runtime = await this.openSession({ kind: "resume", request: params }, signal);
|
|
26486
|
-
|
|
26615
|
+
const configOptions = sessionConfigOptions(runtime.session);
|
|
26616
|
+
return runtime.readOnly ? { configOptions, _meta: { codex: { readOnly: true, reason: "active_writer" } } } : { configOptions };
|
|
26487
26617
|
}
|
|
26488
26618
|
async forkSession(params, signal) {
|
|
26489
26619
|
const runtime = await this.openSession({ kind: "fork", request: params }, signal);
|
|
@@ -26519,7 +26649,7 @@ class CodexAgent {
|
|
|
26519
26649
|
if (open2.kind !== "new")
|
|
26520
26650
|
this.assertUnsubscribeSettled(open2.request.sessionId);
|
|
26521
26651
|
const { request } = open2;
|
|
26522
|
-
if (typeof request.cwd !== "string" || !
|
|
26652
|
+
if (typeof request.cwd !== "string" || !path7.isAbsolute(request.cwd)) {
|
|
26523
26653
|
throw RequestError.invalidParams({ cwd: request.cwd }, "cwd must be an absolute path");
|
|
26524
26654
|
}
|
|
26525
26655
|
if (open2.kind === "resume" && open2.request.replayFrom != null && open2.request.replayFrom.type !== "start") {
|
|
@@ -26532,25 +26662,44 @@ class CodexAgent {
|
|
|
26532
26662
|
this.assertUnsubscribeSettled(open2.request.sessionId);
|
|
26533
26663
|
}
|
|
26534
26664
|
const mcpServers = request.mcpServers ?? [];
|
|
26665
|
+
const requestedModel = requestedModelOf(request._meta);
|
|
26666
|
+
let onGateway = this.providers.routesByDefault();
|
|
26667
|
+
if (this.providers.active !== null && this.providers.mode === "catalog") {
|
|
26668
|
+
if (requestedModel !== null)
|
|
26669
|
+
onGateway = this.providers.isGatewayModel(requestedModel);
|
|
26670
|
+
else if (open2.kind === "resume") {
|
|
26671
|
+
const known = await this.withCodex(() => this.codex.threadRead({ threadId: open2.request.sessionId, includeTurns: false }));
|
|
26672
|
+
onGateway = known.thread.modelProvider === GATEWAY_MODEL_PROVIDER;
|
|
26673
|
+
}
|
|
26674
|
+
}
|
|
26535
26675
|
let accountVersion = this.accountGeneration;
|
|
26536
26676
|
const account = await this.withCodex(() => this.codex.accountRead({ refreshToken: false }));
|
|
26537
|
-
if (
|
|
26677
|
+
if (!onGateway && account.requiresOpenaiAuth && account.account === null) {
|
|
26538
26678
|
throw RequestError.authRequired(undefined, "Log in to Codex first (auth/login)");
|
|
26539
26679
|
}
|
|
26540
26680
|
const existingMcp = mcpServers.length > 0 ? await this.withCodex(() => this.configuredMcpServerNames(request.cwd)) : new Set;
|
|
26541
|
-
const config2 = buildThreadConfig(this.providers.threadConfig(), request.cwd, additionalDirectories, mcpServers, existingMcp);
|
|
26681
|
+
const config2 = buildThreadConfig(this.providers.threadConfig(onGateway), request.cwd, additionalDirectories, mcpServers, existingMcp);
|
|
26542
26682
|
const mcpStartupGeneration = this.codex.mcpStartupGeneration;
|
|
26543
|
-
const modelProvider = await this.withCodex(() => this.resolveModelProvider());
|
|
26683
|
+
const modelProvider = onGateway ? GATEWAY_MODEL_PROVIDER : await this.withCodex(() => this.resolveModelProvider());
|
|
26684
|
+
const startModel = onGateway ? requestedModel ?? this.providers.active?.model ?? null : requestedModel;
|
|
26685
|
+
let readOnly = false;
|
|
26544
26686
|
const { thread, skills } = await this.withSkillsContext(request.cwd, additionalDirectories, async (skills2) => ({ skills: skills2, thread: await this.withCodex(async () => {
|
|
26545
26687
|
if (signal?.aborted)
|
|
26546
26688
|
throw RequestError.requestCancelled(undefined, "Session opening was cancelled");
|
|
26547
26689
|
switch (open2.kind) {
|
|
26548
26690
|
case "new":
|
|
26549
|
-
return await this.codex.threadStart({ config: config2, cwd: request.cwd, modelProvider });
|
|
26691
|
+
return await this.codex.threadStart({ config: config2, cwd: request.cwd, modelProvider, model: startModel });
|
|
26550
26692
|
case "resume":
|
|
26551
|
-
|
|
26693
|
+
try {
|
|
26694
|
+
return await this.codex.threadResume({ threadId: open2.request.sessionId, config: config2, cwd: request.cwd, modelProvider, model: startModel, excludeTurns: true });
|
|
26695
|
+
} catch (error40) {
|
|
26696
|
+
if (!errorMessage(error40).includes("already has an active writer"))
|
|
26697
|
+
throw error40;
|
|
26698
|
+
readOnly = true;
|
|
26699
|
+
return readOnlyThread((await this.codex.threadRead({ threadId: open2.request.sessionId, includeTurns: false })).thread, request.cwd);
|
|
26700
|
+
}
|
|
26552
26701
|
case "fork":
|
|
26553
|
-
return await this.codex.threadFork({ threadId: open2.request.sessionId, config: config2, cwd: request.cwd, modelProvider, excludeTurns: true });
|
|
26702
|
+
return await this.codex.threadFork({ threadId: open2.request.sessionId, config: config2, cwd: request.cwd, modelProvider, model: startModel, excludeTurns: true });
|
|
26554
26703
|
}
|
|
26555
26704
|
}) }));
|
|
26556
26705
|
const sessionId = thread.thread.id;
|
|
@@ -26578,15 +26727,15 @@ class CodexAgent {
|
|
|
26578
26727
|
accountVersion = this.accountGeneration;
|
|
26579
26728
|
openedAccount = (await this.withCodex(() => this.codex.accountRead({ refreshToken: false }))).account;
|
|
26580
26729
|
}
|
|
26581
|
-
const gateway = this.providers.active;
|
|
26582
26730
|
const catalog = this.providers.catalog(codexCatalog);
|
|
26583
|
-
const model = resolveModelSelection(catalog,
|
|
26731
|
+
const model = resolveModelSelection(catalog, startModel ?? thread.model, thread.reasoningEffort);
|
|
26584
26732
|
const session = {
|
|
26585
26733
|
id: sessionId,
|
|
26586
26734
|
cwd: request.cwd,
|
|
26587
26735
|
additionalDirectories,
|
|
26588
26736
|
mcpServerNames: mcpServerNames(mcpServers).filter((name) => !existingMcp.has(name)),
|
|
26589
26737
|
catalog,
|
|
26738
|
+
gatewayGroup: this.providers.gatewayGroup(),
|
|
26590
26739
|
model,
|
|
26591
26740
|
mode: initialAgentMode(this.env),
|
|
26592
26741
|
collaborationMode: DEFAULT_COLLABORATION_MODE,
|
|
@@ -26599,7 +26748,7 @@ class CodexAgent {
|
|
|
26599
26748
|
contextWindow: null,
|
|
26600
26749
|
closed: false
|
|
26601
26750
|
};
|
|
26602
|
-
const runtime = this.installRuntime(session, capabilities, config2, thread.modelProvider);
|
|
26751
|
+
const runtime = this.installRuntime(session, capabilities, config2, onGateway ? GATEWAY_MODEL_PROVIDER : thread.modelProvider, readOnly);
|
|
26603
26752
|
const cancelOpening = () => {
|
|
26604
26753
|
runtime.session.closed = true;
|
|
26605
26754
|
runtime.lifetime.abort();
|
|
@@ -26646,7 +26795,7 @@ class CodexAgent {
|
|
|
26646
26795
|
stopObservingOpen();
|
|
26647
26796
|
}
|
|
26648
26797
|
}
|
|
26649
|
-
installRuntime(session, capabilities, config2, modelProvider) {
|
|
26798
|
+
installRuntime(session, capabilities, config2, modelProvider, readOnly = false) {
|
|
26650
26799
|
const client = new ClientSession(session.id, this.link, capabilities);
|
|
26651
26800
|
const bridge = new EventBridge(client, session);
|
|
26652
26801
|
const turnContext = new TurnContext(session.id);
|
|
@@ -26656,7 +26805,7 @@ class CodexAgent {
|
|
|
26656
26805
|
};
|
|
26657
26806
|
const approval = new CodexApprovalHandler(client, turnContext, signal);
|
|
26658
26807
|
const elicitation = new CodexElicitationHandler(client, turnContext, signal);
|
|
26659
|
-
const runtime = { config: config2, modelProvider, stale: false, session, client, bridge, turnContext, elicitation, lifetime: new AbortController, queue: Promise.resolve() };
|
|
26808
|
+
const runtime = { config: config2, modelProvider, stale: false, readOnly, session, client, bridge, turnContext, elicitation, lifetime: new AbortController, queue: Promise.resolve() };
|
|
26660
26809
|
const drained = (operation) => async (params) => {
|
|
26661
26810
|
await abortable(this.drain(runtime), this.codex.disconnectSignal);
|
|
26662
26811
|
return await operation(params);
|
|
@@ -26923,7 +27072,7 @@ class CodexAgent {
|
|
|
26923
27072
|
runtime.session.activeTurn = null;
|
|
26924
27073
|
turn.resolveFinished();
|
|
26925
27074
|
}
|
|
26926
|
-
const unsubscribe = this.unsubscribeThread(sessionId);
|
|
27075
|
+
const unsubscribe = runtime.readOnly ? Promise.resolve() : this.unsubscribeThread(sessionId);
|
|
26927
27076
|
let remoteUnsubscribe = "confirmed";
|
|
26928
27077
|
try {
|
|
26929
27078
|
if (!await within(unsubscribe, Math.max(0, deadline - performance.now()))) {
|
|
@@ -26952,11 +27101,46 @@ class CodexAgent {
|
|
|
26952
27101
|
}
|
|
26953
27102
|
async setSessionConfigOptionInternal(params) {
|
|
26954
27103
|
const runtime = this.runtime(params.sessionId, "session/set_config_option");
|
|
27104
|
+
assertWritable(runtime);
|
|
27105
|
+
if (params.configId === MODEL_CONFIG_ID && this.providers.mode === "catalog" && typeof params.value === "string") {
|
|
27106
|
+
const toGateway = this.providers.isGatewayModel(params.value);
|
|
27107
|
+
if (toGateway !== (runtime.modelProvider === GATEWAY_MODEL_PROVIDER))
|
|
27108
|
+
await this.rerouteSession(runtime, toGateway, params.value);
|
|
27109
|
+
}
|
|
26955
27110
|
await this.withCodex(() => applyConfigOption(runtime.session, this.codex, params));
|
|
26956
27111
|
const configOptions = sessionConfigOptions(runtime.session);
|
|
26957
27112
|
await runtime.client.update({ sessionUpdate: "config_option_update", configOptions });
|
|
26958
27113
|
return { configOptions };
|
|
26959
27114
|
}
|
|
27115
|
+
async rerouteSession(runtime, toGateway, modelId) {
|
|
27116
|
+
const { session } = runtime;
|
|
27117
|
+
this.assertRoutingAvailable();
|
|
27118
|
+
if (session.activeTurn !== null) {
|
|
27119
|
+
throw RequestError.invalidRequest({ sessionId: session.id }, "The model provider cannot change while a turn is running; cancel it or wait for idle");
|
|
27120
|
+
}
|
|
27121
|
+
try {
|
|
27122
|
+
await this.codex.threadTurnsList({ threadId: session.id, limit: 1, itemsView: "notLoaded" });
|
|
27123
|
+
} catch {
|
|
27124
|
+
await this.withCodex(() => this.codex.threadInjectItems({ threadId: session.id, items: [providerChangeNote()] }));
|
|
27125
|
+
}
|
|
27126
|
+
const config2 = this.providers.routeConfig(runtime.config, toGateway);
|
|
27127
|
+
const modelProvider = toGateway ? GATEWAY_MODEL_PROVIDER : await this.withCodex(() => this.resolveModelProvider());
|
|
27128
|
+
await this.codex.threadUnsubscribe({ threadId: session.id });
|
|
27129
|
+
try {
|
|
27130
|
+
await this.withCodex(() => this.codex.threadResume({ threadId: session.id, cwd: session.cwd, config: config2, modelProvider, model: modelId, excludeTurns: true }));
|
|
27131
|
+
} catch (error40) {
|
|
27132
|
+
try {
|
|
27133
|
+
await this.codex.threadResume({ threadId: session.id, cwd: session.cwd, config: runtime.config, modelProvider: runtime.modelProvider, model: session.model.model, excludeTurns: true });
|
|
27134
|
+
} catch (rollbackError) {
|
|
27135
|
+
runtime.stale = true;
|
|
27136
|
+
logger.error("Provider rollback failed", rollbackError, { sessionId: session.id });
|
|
27137
|
+
}
|
|
27138
|
+
throw RequestError.internalError({ sessionId: session.id, stale: runtime.stale }, `Could not move the session to ${toGateway ? "the gateway" : "Codex's provider"}: ${errorMessage(error40)}`);
|
|
27139
|
+
}
|
|
27140
|
+
runtime.config = config2;
|
|
27141
|
+
runtime.modelProvider = modelProvider;
|
|
27142
|
+
runtime.stale = false;
|
|
27143
|
+
}
|
|
26960
27144
|
runtime(sessionId, method) {
|
|
26961
27145
|
this.requireInitialized(method);
|
|
26962
27146
|
const runtime = this.sessions.get(sessionId);
|
|
@@ -26972,6 +27156,7 @@ class CodexAgent {
|
|
|
26972
27156
|
if (!allowReentry && this.sessionMutations.has(params.sessionId))
|
|
26973
27157
|
throw RequestError.invalidRequest({ sessionId: params.sessionId }, "Session lifecycle or configuration work is in progress; retry when it finishes");
|
|
26974
27158
|
const runtime = this.runtime(params.sessionId, "session/prompt");
|
|
27159
|
+
assertWritable(runtime);
|
|
26975
27160
|
if (runtime.stale)
|
|
26976
27161
|
throw RequestError.invalidRequest({ sessionId: params.sessionId, _meta: { codex: { routing: { stale: true } } } }, "Session routing is stale; resume the session or successfully change providers before prompting");
|
|
26977
27162
|
if (!Array.isArray(params.prompt) || params.prompt.length === 0) {
|
|
@@ -27270,7 +27455,7 @@ ${error40.additionalDetails}` : error40.message;
|
|
|
27270
27455
|
}
|
|
27271
27456
|
withSkillsContext(cwd, additionalDirectories, operation) {
|
|
27272
27457
|
const run = async () => {
|
|
27273
|
-
const roots = additionalDirectories.map((root) =>
|
|
27458
|
+
const roots = additionalDirectories.map((root) => path7.join(root, ".agents", "skills"));
|
|
27274
27459
|
if (roots.length !== this.skillRoots.length || roots.some((root, index) => root !== this.skillRoots[index])) {
|
|
27275
27460
|
this.changingSkillRoots = true;
|
|
27276
27461
|
try {
|
|
@@ -27304,7 +27489,7 @@ ${error40.additionalDetails}` : error40.message;
|
|
|
27304
27489
|
}
|
|
27305
27490
|
async refreshAvailableCommands(runtime) {
|
|
27306
27491
|
const run = async () => {
|
|
27307
|
-
const roots = runtime.session.additionalDirectories.map((root) =>
|
|
27492
|
+
const roots = runtime.session.additionalDirectories.map((root) => path7.join(root, ".agents", "skills"));
|
|
27308
27493
|
if (runtime.session.closed || roots.length !== this.skillRoots.length || roots.some((root, i) => root !== this.skillRoots[i]))
|
|
27309
27494
|
return;
|
|
27310
27495
|
return await this.loadSkills(runtime.session.cwd, runtime.session.additionalDirectories);
|
|
@@ -27428,6 +27613,34 @@ function archivedFilter(meta) {
|
|
|
27428
27613
|
const codex = meta?.codex;
|
|
27429
27614
|
return codex?.archived === true;
|
|
27430
27615
|
}
|
|
27616
|
+
function providerChangeNote() {
|
|
27617
|
+
return { type: "message", role: "developer", content: [{ type: "input_text", text: "The user changed this conversation's model before sending a message." }] };
|
|
27618
|
+
}
|
|
27619
|
+
function assertWritable(runtime) {
|
|
27620
|
+
if (!runtime.readOnly)
|
|
27621
|
+
return;
|
|
27622
|
+
throw RequestError.invalidRequest({ codex: { readOnly: true } }, "Thread is open in another Codex client");
|
|
27623
|
+
}
|
|
27624
|
+
function readOnlyThread(thread, cwd) {
|
|
27625
|
+
return {
|
|
27626
|
+
thread,
|
|
27627
|
+
model: thread.model ?? "",
|
|
27628
|
+
modelProvider: thread.modelProvider,
|
|
27629
|
+
serviceTier: null,
|
|
27630
|
+
cwd,
|
|
27631
|
+
instructionSources: [],
|
|
27632
|
+
approvalPolicy: "on-request",
|
|
27633
|
+
approvalsReviewer: "user",
|
|
27634
|
+
sandbox: { type: "readOnly", networkAccess: false },
|
|
27635
|
+
reasoningEffort: thread.reasoningEffort,
|
|
27636
|
+
turnsBackwardsCursor: null,
|
|
27637
|
+
itemsBackwardsCursor: null
|
|
27638
|
+
};
|
|
27639
|
+
}
|
|
27640
|
+
function requestedModelOf(meta) {
|
|
27641
|
+
const raw = meta?.alwith?.model;
|
|
27642
|
+
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
27643
|
+
}
|
|
27431
27644
|
function seedHistoryOf(meta) {
|
|
27432
27645
|
const raw = meta?.codex?.seedHistory;
|
|
27433
27646
|
if (raw === undefined || raw === null)
|
|
@@ -27922,7 +28135,7 @@ function threadIdOf(notification) {
|
|
|
27922
28135
|
|
|
27923
28136
|
// src/codex/process.ts
|
|
27924
28137
|
import { existsSync } from "fs";
|
|
27925
|
-
import
|
|
28138
|
+
import path8 from "path";
|
|
27926
28139
|
import { spawn, spawnSync } from "child_process";
|
|
27927
28140
|
import { createRequire } from "module";
|
|
27928
28141
|
|
|
@@ -28215,11 +28428,11 @@ function resolveCodexLauncher(codexPath) {
|
|
|
28215
28428
|
const target = targets[platform2];
|
|
28216
28429
|
if (!target)
|
|
28217
28430
|
throw new Error(`Unsupported Codex platform: ${platform2}`);
|
|
28218
|
-
let vendor =
|
|
28431
|
+
let vendor = path8.resolve(path8.dirname(launcher), "..", "vendor");
|
|
28219
28432
|
try {
|
|
28220
|
-
vendor =
|
|
28433
|
+
vendor = path8.join(path8.dirname(platformRequire.resolve(`@openai/codex-${platform2}/package.json`)), "vendor");
|
|
28221
28434
|
} catch {}
|
|
28222
|
-
const command =
|
|
28435
|
+
const command = path8.join(vendor, target, "bin", process.platform === "win32" ? "codex.exe" : "codex");
|
|
28223
28436
|
if (!existsSync(command))
|
|
28224
28437
|
throw new Error(`Missing Codex platform binary: ${command}`);
|
|
28225
28438
|
return { command, prefixArgs: [], shell: false };
|