@desplega.ai/agent-swarm 1.74.3 → 1.74.4

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/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Swarm API",
5
- "version": "1.74.3",
5
+ "version": "1.74.4",
6
6
  "description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
7
7
  },
8
8
  "servers": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.74.3",
3
+ "version": "1.74.4",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
@@ -66,13 +66,7 @@ import {
66
66
  } from "@openai/codex-sdk";
67
67
  import { scrubSecrets } from "../utils/secret-scrubber";
68
68
  import { type CodexAgentsMdHandle, writeCodexAgentsMd } from "./codex-agents-md";
69
- import {
70
- CODEX_DEFAULT_MODEL,
71
- type CodexModel,
72
- computeCodexCostUsd,
73
- getCodexContextWindow,
74
- resolveCodexModel,
75
- } from "./codex-models";
69
+ import { computeCodexCostUsd, getCodexContextWindow, resolveCodexModel } from "./codex-models";
76
70
  import { credentialsToAuthJson } from "./codex-oauth/auth-json.js";
77
71
  import { getValidCodexOAuth } from "./codex-oauth/storage.js";
78
72
  import { resolveCodexPrompt } from "./codex-skill-resolver";
@@ -111,6 +105,68 @@ interface InstalledMcpServersResponse {
111
105
  total?: number;
112
106
  }
113
107
 
108
+ /**
109
+ * Resolve which Codex auth mode is active for the spawned subprocess and,
110
+ * if needed, restore ChatGPT OAuth credentials from the swarm config store
111
+ * to `~/.codex/auth.json`.
112
+ *
113
+ * Precedence (matches `docker-entrypoint.sh`): `codex_oauth` from the swarm
114
+ * config store > `OPENAI_API_KEY` env var. If both exist, OAuth wins — and
115
+ * if a stale api-key-mode `auth.json` is present, it gets overwritten with
116
+ * the OAuth payload.
117
+ *
118
+ * Returns the `auth_mode` value the spawned Codex CLI will see, or `null`
119
+ * if no `auth.json` exists (Codex will then fall back to `OPENAI_API_KEY`).
120
+ */
121
+ async function resolveCodexAuthMode(
122
+ config: ProviderSessionConfig,
123
+ emit: (event: ProviderEvent) => void,
124
+ ): Promise<string | null> {
125
+ const fs = await import("node:fs/promises");
126
+ const authJsonPath = join(os.homedir(), ".codex", "auth.json");
127
+
128
+ const readAuthMode = async (): Promise<string | null> => {
129
+ try {
130
+ const raw = await fs.readFile(authJsonPath, "utf-8");
131
+ const parsed = JSON.parse(raw) as { auth_mode?: unknown };
132
+ return typeof parsed.auth_mode === "string" ? parsed.auth_mode : null;
133
+ } catch {
134
+ return null;
135
+ }
136
+ };
137
+
138
+ let currentMode = await readAuthMode();
139
+
140
+ // If config store creds are available and auth.json is missing or in
141
+ // api-key mode, try to restore/upgrade to OAuth. Don't touch a file that's
142
+ // already in chatgpt mode — `getValidCodexOAuth` refreshes and writes back
143
+ // to the config store on its own when called next time.
144
+ if (config.apiUrl && config.apiKey && currentMode !== "chatgpt") {
145
+ const oauthCreds = await getValidCodexOAuth(config.apiUrl, config.apiKey);
146
+ if (oauthCreds) {
147
+ try {
148
+ const authJson = credentialsToAuthJson(oauthCreds);
149
+ await fs.mkdir(join(os.homedir(), ".codex"), { recursive: true, mode: 0o700 });
150
+ await fs.writeFile(authJsonPath, JSON.stringify(authJson, null, 2), { mode: 0o600 });
151
+ const verb = currentMode === null ? "Restored" : "Upgraded api-key auth.json to";
152
+ emit({
153
+ type: "raw_stderr",
154
+ content: `[codex] ${verb} OAuth credentials from config store\n`,
155
+ });
156
+ currentMode = "chatgpt";
157
+ } catch (err) {
158
+ const message = err instanceof Error ? err.message : String(err);
159
+ emit({
160
+ type: "raw_stderr",
161
+ content: `[codex] Failed to write auth.json: ${message}\n`,
162
+ });
163
+ }
164
+ }
165
+ }
166
+
167
+ return currentMode;
168
+ }
169
+
114
170
  /**
115
171
  * Build the per-session Codex config object, which becomes the
116
172
  * `config` option to `new Codex({ config })`. This layers on top of the
@@ -131,7 +187,7 @@ interface InstalledMcpServersResponse {
131
187
  */
132
188
  export async function buildCodexConfig(
133
189
  config: ProviderSessionConfig,
134
- model: CodexModel,
190
+ model: string,
135
191
  emit: (event: ProviderEvent) => void,
136
192
  ): Promise<CodexConfig> {
137
193
  const mcpServers: Record<string, Record<string, unknown>> = {};
@@ -247,7 +303,7 @@ class CodexSession implements ProviderSession {
247
303
  private readonly thread: Thread;
248
304
  private readonly config: ProviderSessionConfig;
249
305
  private readonly agentsMdHandle: CodexAgentsMdHandle;
250
- private readonly resolvedModel: CodexModel;
306
+ private readonly resolvedModel: string;
251
307
  private readonly contextWindow: number;
252
308
  private readonly skillsDir: string;
253
309
  private readonly listeners: Array<(event: ProviderEvent) => void> = [];
@@ -273,7 +329,7 @@ class CodexSession implements ProviderSession {
273
329
  thread: Thread,
274
330
  config: ProviderSessionConfig,
275
331
  agentsMdHandle: CodexAgentsMdHandle,
276
- resolvedModel: CodexModel,
332
+ resolvedModel: string,
277
333
  initialEvents: ProviderEvent[] = [],
278
334
  skillsDir?: string,
279
335
  ) {
@@ -763,8 +819,9 @@ export class CodexAdapter implements ProviderAdapter {
763
819
  const agentsMdHandle = await writeCodexAgentsMd(config.cwd, config.systemPrompt);
764
820
 
765
821
  try {
766
- // Resolve the model once and thread it through. Unknown values fall
767
- // back to `CODEX_DEFAULT_MODEL` (see `codex-models.ts`).
822
+ // Resolve the model once and thread it through. Claude shortnames map
823
+ // to Codex equivalents; everything else passes through verbatim — the
824
+ // SDK is the source of truth for what's valid.
768
825
  const resolvedModel = resolveCodexModel(config.model);
769
826
 
770
827
  // Buffer warnings emitted during config-building so they're not lost
@@ -776,75 +833,32 @@ export class CodexAdapter implements ProviderAdapter {
776
833
  preSessionEvents.push(event);
777
834
  };
778
835
 
779
- // Warn (as a buffered event) if the caller passed a model that didn't
780
- // round-trip through `resolveCodexModel`. This catches typos early.
781
- if (
782
- config.model &&
783
- config.model.toLowerCase() !== resolvedModel &&
784
- !["opus", "sonnet", "haiku"].includes(config.model.toLowerCase())
785
- ) {
786
- bufferedEmit({
787
- type: "raw_stderr",
788
- content: `[codex] Unknown model "${config.model}" — falling back to ${CODEX_DEFAULT_MODEL}. See src/providers/codex-models.ts for the supported list.\n`,
789
- });
790
- }
791
-
792
836
  const mergedConfig = await buildCodexConfig(config, resolvedModel, bufferedEmit);
793
837
 
838
+ // Auth resolution. `codex_oauth` (in the swarm config store) wins over
839
+ // `OPENAI_API_KEY` so users can keep an OpenAI key set for embeddings
840
+ // without it shadowing their ChatGPT login. The entrypoint already runs
841
+ // this same precedence at boot — this block handles local dev (where
842
+ // the entrypoint didn't run) and any case where auth.json is stale.
843
+ const authMode = await resolveCodexAuthMode(config, bufferedEmit);
844
+
794
845
  // `CodexOptions.env` does NOT inherit from `process.env`. Construct a
795
- // minimal env explicitly so the spawned Codex CLI can still find its
796
- // binary (PATH), write to HOME, and authenticate (OPENAI_API_KEY).
797
- // Merge anything the runner passed in `config.env` on top.
846
+ // minimal env explicitly so the spawned Codex CLI can find its binary
847
+ // (PATH) and HOME (for ~/.codex/auth.json). `OPENAI_API_KEY` is only
848
+ // forwarded when auth.json is NOT in chatgpt mode — otherwise it would
849
+ // override the OAuth login at the Codex CLI layer.
798
850
  const env: Record<string, string> = {
799
851
  PATH: process.env.PATH ?? "",
800
852
  HOME: process.env.HOME ?? "",
801
- ...(process.env.OPENAI_API_KEY ? { OPENAI_API_KEY: process.env.OPENAI_API_KEY } : {}),
853
+ ...(authMode !== "chatgpt" && process.env.OPENAI_API_KEY
854
+ ? { OPENAI_API_KEY: process.env.OPENAI_API_KEY }
855
+ : {}),
802
856
  ...(process.env.NODE_EXTRA_CA_CERTS
803
857
  ? { NODE_EXTRA_CA_CERTS: process.env.NODE_EXTRA_CA_CERTS }
804
858
  : {}),
805
859
  ...(config.env ?? {}),
806
860
  };
807
861
 
808
- // OAuth credential resolution: if no OPENAI_API_KEY is set, try to
809
- // restore or refresh ChatGPT OAuth credentials from the config store.
810
- // The entrypoint also restores at boot, but this handles cases where
811
- // the entrypoint didn't run (local dev) or tokens expired mid-session.
812
- if (!process.env.OPENAI_API_KEY && config.apiUrl && config.apiKey) {
813
- const authJsonPath = join(os.homedir(), ".codex", "auth.json");
814
- let hasAuth = false;
815
- try {
816
- const fs = await import("node:fs/promises");
817
- await fs.access(authJsonPath);
818
- hasAuth = true;
819
- } catch {
820
- // auth.json doesn't exist
821
- }
822
-
823
- if (!hasAuth) {
824
- const oauthCreds = await getValidCodexOAuth(config.apiUrl, config.apiKey);
825
- if (oauthCreds) {
826
- try {
827
- const fs = await import("node:fs/promises");
828
- const authJson = credentialsToAuthJson(oauthCreds);
829
- await fs.mkdir(join(os.homedir(), ".codex"), { recursive: true, mode: 0o700 });
830
- await fs.writeFile(authJsonPath, JSON.stringify(authJson, null, 2), {
831
- mode: 0o600,
832
- });
833
- bufferedEmit({
834
- type: "raw_stderr",
835
- content: "[codex] Restored OAuth credentials from config store\n",
836
- });
837
- } catch (err) {
838
- const message = err instanceof Error ? err.message : String(err);
839
- bufferedEmit({
840
- type: "raw_stderr",
841
- content: `[codex] Failed to write auth.json: ${message}\n`,
842
- });
843
- }
844
- }
845
- }
846
- }
847
-
848
862
  // The SDK's default `findCodexPath()` does `require.resolve("@openai/codex")`
849
863
  // from the SDK's own module. When agent-swarm runs as a Bun single-file
850
864
  // compiled executable, the bundled SDK can't resolve `@openai/codex` at
@@ -11,7 +11,12 @@
11
11
  * pulling in the SDK.
12
12
  */
13
13
 
14
- /** List of Codex models that can be selected via `ThreadOptions.model`. */
14
+ /**
15
+ * List of Codex models we know about (drives the onboarding model selector,
16
+ * the pricing table, and the context-window map). The resolver does NOT
17
+ * constrain inputs to this list — it passes unknown strings through to the
18
+ * SDK, so new OpenAI models work without a code change.
19
+ */
15
20
  export const CODEX_MODELS = [
16
21
  "gpt-5.4", // default — mainline reasoning model w/ frontier coding
17
22
  "gpt-5.4-mini", // faster/cheaper
@@ -29,25 +34,24 @@ export const CODEX_DEFAULT_MODEL: CodexModel = "gpt-5.4";
29
34
  * to Codex equivalents. Mirrors `pi-mono-adapter.ts:71-75` shortnames map so
30
35
  * a task authored for Claude works unchanged when pointed at a Codex worker.
31
36
  */
32
- const SHORTNAME_TO_CODEX: Record<string, CodexModel> = {
37
+ const CLAUDE_SHORTNAMES: Record<string, CodexModel> = {
33
38
  opus: "gpt-5.4",
34
- sonnet: "gpt-5.4-mini",
39
+ sonnet: "gpt-5.4",
35
40
  haiku: "gpt-5.4-mini",
36
- // explicit passthrough entries so MODEL_OVERRIDE="gpt-5.4" round-trips
37
- "gpt-5.4": "gpt-5.4",
38
- "gpt-5.4-mini": "gpt-5.4-mini",
39
- "gpt-5.3-codex": "gpt-5.3-codex",
40
- "gpt-5.2-codex": "gpt-5.2-codex",
41
41
  };
42
42
 
43
43
  /**
44
- * Resolve an arbitrary model string (shortname or full Codex model id) into
45
- * a supported `CodexModel`. Unknown values fall back to `CODEX_DEFAULT_MODEL`.
44
+ * Resolve a model string (shortname or full Codex model id) into the literal
45
+ * id we hand to the Codex SDK. Behavior:
46
+ * - empty/undefined → `CODEX_DEFAULT_MODEL`
47
+ * - claude shortname (opus/sonnet/haiku) → mapped Codex id
48
+ * - anything else → passthrough (lowercased), so new OpenAI models work
49
+ * without a code change. The SDK is the source of truth for validity.
46
50
  */
47
- export function resolveCodexModel(modelStr: string | undefined): CodexModel {
51
+ export function resolveCodexModel(modelStr: string | undefined): string {
48
52
  if (!modelStr) return CODEX_DEFAULT_MODEL;
49
53
  const normalized = modelStr.toLowerCase();
50
- return SHORTNAME_TO_CODEX[normalized] ?? CODEX_DEFAULT_MODEL;
54
+ return CLAUDE_SHORTNAMES[normalized] ?? normalized;
51
55
  }
52
56
 
53
57
  /**
@@ -65,9 +69,13 @@ export const CODEX_MODEL_CONTEXT_WINDOWS: Record<CodexModel, number> = {
65
69
  "gpt-5.2-codex": 200_000,
66
70
  };
67
71
 
68
- /** Return the context window in tokens for a given Codex model. */
69
- export function getCodexContextWindow(model: CodexModel): number {
70
- return CODEX_MODEL_CONTEXT_WINDOWS[model] ?? 200_000;
72
+ /**
73
+ * Return the context window in tokens for a given Codex model. Unknown models
74
+ * (passthrough strings) get the 200k default — keeps `context_usage` finite
75
+ * even on a model id we haven't catalogued yet.
76
+ */
77
+ export function getCodexContextWindow(model: string): number {
78
+ return CODEX_MODEL_CONTEXT_WINDOWS[model as CodexModel] ?? 200_000;
71
79
  }
72
80
 
73
81
  /**
@@ -126,12 +134,12 @@ export const CODEX_MODEL_PRICING: Record<CodexModel, CodexModelPricing> = {
126
134
  * inflate cost on a typo.
127
135
  */
128
136
  export function computeCodexCostUsd(
129
- model: CodexModel,
137
+ model: string,
130
138
  inputTokens: number,
131
139
  cachedInputTokens: number,
132
140
  outputTokens: number,
133
141
  ): number {
134
- const pricing = CODEX_MODEL_PRICING[model];
142
+ const pricing = CODEX_MODEL_PRICING[model as CodexModel];
135
143
  if (!pricing) return 0;
136
144
  const uncachedInput = Math.max(0, inputTokens - cachedInputTokens);
137
145
  const inputCost = (uncachedInput / 1_000_000) * pricing.inputPerMillion;
@@ -628,8 +628,8 @@ describe("resolveCodexModel", () => {
628
628
  expect(resolveCodexModel("opus")).toBe("gpt-5.4");
629
629
  });
630
630
 
631
- test("claude shortname 'sonnet' → gpt-5.4-mini", () => {
632
- expect(resolveCodexModel("sonnet")).toBe("gpt-5.4-mini");
631
+ test("claude shortname 'sonnet' → gpt-5.4", () => {
632
+ expect(resolveCodexModel("sonnet")).toBe("gpt-5.4");
633
633
  });
634
634
 
635
635
  test("claude shortname 'haiku' → gpt-5.4-mini", () => {
@@ -652,8 +652,9 @@ describe("resolveCodexModel", () => {
652
652
  expect(resolveCodexModel("GPT-5.4")).toBe("gpt-5.4");
653
653
  });
654
654
 
655
- test("unknown model CODEX_DEFAULT_MODEL", () => {
656
- expect(resolveCodexModel("unknown-model")).toBe(CODEX_DEFAULT_MODEL);
655
+ test("unknown model passes through verbatim (lowercased)", () => {
656
+ expect(resolveCodexModel("gpt-5.5-experimental")).toBe("gpt-5.5-experimental");
657
+ expect(resolveCodexModel("GPT-9-FUTURE")).toBe("gpt-9-future");
657
658
  });
658
659
  });
659
660