talon-agent 5.25.1 → 5.26.1
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 +1 -0
- package/package.json +1 -1
- package/src/backend/agy/one-shot.ts +34 -3
- package/src/backend/claude-sdk/model-provider.ts +59 -12
- package/src/backend/codex/auth.ts +31 -2
- package/src/backend/codex/constants.ts +21 -7
- package/src/backend/codex/discovery.ts +32 -1
- package/src/backend/codex/factory.ts +3 -5
- package/src/backend/codex/handler/message.ts +7 -7
- package/src/backend/codex/models.ts +53 -11
- package/src/backend/codex/one-shot.ts +139 -37
- package/src/backend/codex/state.ts +11 -0
- package/src/core/agents/abort-reason.ts +46 -0
- package/src/core/agents/registry.ts +3 -2
- package/src/core/background/dream/index.ts +2 -2
- package/src/core/background/heartbeat/agent.ts +1 -1
- package/src/core/background/isolated-agent.ts +1 -1
- package/src/core/backup/index.ts +1 -0
- package/src/core/backup/status.ts +30 -1
- package/src/core/config/index.ts +8 -0
- package/src/core/engine/gateway.ts +5 -0
- package/src/core/frontend-runtime/admin-notify.ts +46 -6
- package/src/core/frontend-runtime/alerts.ts +10 -6
- package/src/core/mesh/credentials/store.ts +60 -9
- package/src/core/models/active-model.ts +57 -48
- package/src/core/plugin/loader.ts +47 -12
- package/src/core/tools/bridge.ts +58 -4
- package/src/frontend/discord/callbacks/components/effort.ts +4 -4
- package/src/frontend/discord/callbacks/components/index.ts +2 -0
- package/src/frontend/discord/commands/backup-panel.ts +219 -0
- package/src/frontend/discord/commands/backup.ts +25 -34
- package/src/frontend/discord/commands/info.ts +17 -5
- package/src/frontend/discord/commands/settings.ts +12 -9
- package/src/frontend/discord/render.ts +1 -1
- package/src/frontend/native/bridge/credentials/claims.ts +5 -5
- package/src/frontend/native/bridge/routes/chats.ts +40 -20
- package/src/frontend/native/bridge/routes/host.ts +15 -2
- package/src/frontend/native/bridge/routes/table.ts +4 -0
- package/src/frontend/native/commands/admin.ts +64 -0
- package/src/frontend/native/commands/backup.ts +191 -0
- package/src/frontend/native/commands/definitions.ts +113 -0
- package/src/frontend/native/commands/format.ts +24 -0
- package/src/frontend/native/commands/index.ts +106 -0
- package/src/frontend/native/commands/info.ts +97 -0
- package/src/frontend/native/commands/session.ts +130 -0
- package/src/frontend/native/commands/types.ts +26 -0
- package/src/frontend/native/protocol.ts +18 -0
- package/src/frontend/native/surface/handlers.ts +14 -1
- package/src/frontend/native/surface/status.ts +9 -1
- package/src/frontend/native/turn/emit.ts +32 -1
- package/src/frontend/presentation/backup-panel.ts +425 -0
- package/src/frontend/presentation/memory-report.ts +109 -0
- package/src/frontend/presentation/text-commands.ts +243 -0
- package/src/frontend/telegram/actions/media.ts +60 -19
- package/src/frontend/telegram/actions/messaging.ts +9 -0
- package/src/frontend/telegram/admin/sessions.ts +20 -5
- package/src/frontend/telegram/admin.ts +9 -2
- package/src/frontend/telegram/callbacks/backup.ts +153 -22
- package/src/frontend/telegram/callbacks/effort.ts +5 -5
- package/src/frontend/telegram/callbacks/index.ts +2 -2
- package/src/frontend/telegram/callbacks/settings.ts +3 -40
- package/src/frontend/telegram/commands/admin.ts +8 -7
- package/src/frontend/telegram/commands/backup.ts +56 -46
- package/src/frontend/telegram/commands/index.ts +3 -2
- package/src/frontend/telegram/commands/info.ts +45 -26
- package/src/frontend/telegram/commands/memory.ts +8 -92
- package/src/frontend/telegram/commands/settings.ts +13 -45
- package/src/frontend/telegram/commands/whatsapp-pairing.ts +19 -15
- package/src/frontend/telegram/render/backup-panel.ts +47 -0
- package/src/frontend/telegram/render/menu.ts +21 -30
- package/src/frontend/terminal/builtins/model.ts +20 -11
- package/src/frontend/whatsapp/commands.ts +16 -216
- package/src/frontend/whatsapp/messages/inbound.ts +33 -2
package/README.md
CHANGED
|
@@ -487,6 +487,7 @@ Config file: `~/.talon/config.json`
|
|
|
487
487
|
| `model` | `"default"` | Default model. Interpretation depends on the active backend. |
|
|
488
488
|
| `agyBinary` | --- | Path to the Antigravity `agy` executable. `AGY_BINARY` overrides it. |
|
|
489
489
|
| `codexApiKey` | --- | Codex-only OpenAI API key. Prefer this over `openaiApiKey` for Codex API-key auth. `codex login` takes precedence over shared `openaiApiKey`. |
|
|
490
|
+
| `codexChatGptDefaultModel` | --- | Model a ChatGPT-login Codex session falls back to when a model is rejected or none is set. Default: the Codex CLI's own default for the account (from `~/.codex/models_cache.json`). `TALON_CODEX_CHATGPT_MODEL` overrides it. |
|
|
490
491
|
| `concurrency` | `1` | Max concurrent AI queries (1--20) |
|
|
491
492
|
| `pulse` | `true` | Periodic group engagement |
|
|
492
493
|
| `heartbeat` | `false` | Background maintenance agent |
|
package/package.json
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type AgyResult,
|
|
34
34
|
type AgyStepUpdate,
|
|
35
35
|
} from "./events.js";
|
|
36
|
+
import { abortLogLine } from "../../core/agents/abort-reason.js";
|
|
36
37
|
|
|
37
38
|
const ts = (): string => new Date().toISOString().slice(11, 19);
|
|
38
39
|
|
|
@@ -257,20 +258,39 @@ export async function runOneShotAgent(
|
|
|
257
258
|
appendLog,
|
|
258
259
|
...(onAssistantText ? { onAssistantText } : {}),
|
|
259
260
|
aborted: abortController.signal.aborted,
|
|
261
|
+
abortSignal: abortController.signal,
|
|
260
262
|
});
|
|
261
263
|
} catch (err) {
|
|
264
|
+
// settleOneShot already logged its own failure — just pass it on.
|
|
265
|
+
if (err instanceof AgyOneShotError) throw err;
|
|
262
266
|
const msg = err instanceof Error ? err.message : String(err);
|
|
263
267
|
if (abortController.signal.aborted || /abort/i.test(msg)) {
|
|
264
|
-
await appendLog(
|
|
268
|
+
await appendLog(
|
|
269
|
+
`\n### [${ts()}] Aborted\n${abortLogLine(abortController.signal)}\n`,
|
|
270
|
+
);
|
|
265
271
|
return;
|
|
266
272
|
}
|
|
267
273
|
logWarn("agent", `agy one-shot run failed: ${msg}`);
|
|
268
274
|
await appendLog(`\n### [${ts()}] Error\n${msg}\n`);
|
|
275
|
+
// Surface it: a swallowed failure is recorded as a successful run by
|
|
276
|
+
// cron, heartbeat and the task table.
|
|
277
|
+
throw new AgyOneShotError(msg, { cause: err });
|
|
269
278
|
} finally {
|
|
270
279
|
unregisterMcpScope(scope);
|
|
271
280
|
}
|
|
272
281
|
}
|
|
273
282
|
|
|
283
|
+
/**
|
|
284
|
+
* An agy one-shot that failed: no result, a non-SUCCESS turn, or a spawn
|
|
285
|
+
* error. Thrown so callers record the run as failed rather than ok.
|
|
286
|
+
*/
|
|
287
|
+
class AgyOneShotError extends Error {
|
|
288
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
289
|
+
super(message, options);
|
|
290
|
+
this.name = "AgyOneShotError";
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
274
294
|
/** Report the run's answer + usage, or its failure, to the log. */
|
|
275
295
|
async function settleOneShot(inputs: {
|
|
276
296
|
outcome: SpawnOutcome;
|
|
@@ -278,10 +298,14 @@ async function settleOneShot(inputs: {
|
|
|
278
298
|
appendLog: (text: string) => Promise<void>;
|
|
279
299
|
onAssistantText?: OneShotAgentParams["onAssistantText"];
|
|
280
300
|
aborted: boolean;
|
|
301
|
+
abortSignal?: AbortSignal;
|
|
281
302
|
}): Promise<OneShotUsage | void> {
|
|
282
303
|
const { outcome, appendLog } = inputs;
|
|
283
304
|
if (inputs.aborted) {
|
|
284
|
-
|
|
305
|
+
const line = inputs.abortSignal
|
|
306
|
+
? abortLogLine(inputs.abortSignal)
|
|
307
|
+
: "Run aborted.";
|
|
308
|
+
await appendLog(`\n### [${ts()}] Aborted\n${line}\n`);
|
|
285
309
|
return;
|
|
286
310
|
}
|
|
287
311
|
if (!outcome.result) {
|
|
@@ -290,7 +314,14 @@ async function settleOneShot(inputs: {
|
|
|
290
314
|
: outcome.stderr.trim() || `agy exited ${outcome.code ?? "n/a"}`;
|
|
291
315
|
logWarn("agent", `agy one-shot produced no result: ${reason}`);
|
|
292
316
|
await appendLog(`\n### [${ts()}] Error\n${reason}\n`);
|
|
293
|
-
|
|
317
|
+
throw new AgyOneShotError(reason);
|
|
318
|
+
}
|
|
319
|
+
if (outcome.result.status && outcome.result.status !== "SUCCESS") {
|
|
320
|
+
// logResult already wrote the "Turn <status>" section.
|
|
321
|
+
const reason =
|
|
322
|
+
outcome.result.error?.trim() || `agy turn ${outcome.result.status}`;
|
|
323
|
+
logWarn("agent", `agy one-shot turn ${outcome.result.status}: ${reason}`);
|
|
324
|
+
throw new AgyOneShotError(reason);
|
|
294
325
|
}
|
|
295
326
|
const response = outcome.result.response ?? "";
|
|
296
327
|
if (response.trim()) {
|
|
@@ -71,19 +71,68 @@ function isSelectedModel(currentModel: string, candidateId: string): boolean {
|
|
|
71
71
|
|
|
72
72
|
// ── Public API ─────────────────────────────────────────────────────────────
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
query: string,
|
|
76
|
-
): Promise<UnifiedModelResolution> {
|
|
77
|
-
const canonicalId = resolveModelId(query);
|
|
78
|
-
const model = getModel(canonicalId);
|
|
74
|
+
const ONE_MILLION_SUFFIX = "[1m]";
|
|
79
75
|
|
|
80
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Split a `[1m]` context-variant suffix off a query ("opus[1m]" →
|
|
78
|
+
* { stem: "opus", oneMillion: true }). Claude Code accepts the suffix on
|
|
79
|
+
* any alias or id; the SDK's `supportedModels()` list does not enumerate
|
|
80
|
+
* the suffixed forms, so the catalog can't match them directly.
|
|
81
|
+
*/
|
|
82
|
+
function splitOneMillion(query: string): { stem: string; oneMillion: boolean } {
|
|
83
|
+
const trimmed = query.trim();
|
|
84
|
+
if (trimmed.toLowerCase().endsWith(ONE_MILLION_SUFFIX)) {
|
|
81
85
|
return {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
storedValue: model.id,
|
|
86
|
+
stem: trimmed.slice(0, -ONE_MILLION_SUFFIX.length).trim(),
|
|
87
|
+
oneMillion: true,
|
|
85
88
|
};
|
|
86
89
|
}
|
|
90
|
+
return { stem: trimmed, oneMillion: false };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Exact-match a query against the registry: its id/alias directly, or —
|
|
95
|
+
* for a `[1m]` query the registry doesn't list — the base model, returned
|
|
96
|
+
* as a 1M variant whose id keeps the suffix so the SDK actually selects
|
|
97
|
+
* the 1M context window.
|
|
98
|
+
*
|
|
99
|
+
* The SDK reports no context-window metadata per model, so there's no
|
|
100
|
+
* evidence to reject a `[1m]` request on; the binary is the authority.
|
|
101
|
+
* The one form refused is `default[1m]`: "default" is a moving target,
|
|
102
|
+
* not an alias Claude Code suffixes.
|
|
103
|
+
*/
|
|
104
|
+
function resolveExactUnified(query: string): UnifiedModelInfo | undefined {
|
|
105
|
+
const direct = getModel(resolveModelId(query));
|
|
106
|
+
if (direct) return toUnified(direct);
|
|
107
|
+
|
|
108
|
+
const { stem, oneMillion } = splitOneMillion(query);
|
|
109
|
+
if (!oneMillion || !stem) return undefined;
|
|
110
|
+
const baseId = resolveModelId(stem);
|
|
111
|
+
const base = getModel(baseId);
|
|
112
|
+
if (!base || stem.toLowerCase() === "default") return undefined;
|
|
113
|
+
|
|
114
|
+
// Pass the SDK a form it knows: the canonical id when it's concrete, the
|
|
115
|
+
// user's own stem when the canonical is the "default" alias (e.g. "opus"
|
|
116
|
+
// folds into "default" when default currently serves Opus).
|
|
117
|
+
const sdkStem = baseId === "default" ? stem : baseId;
|
|
118
|
+
const displayName = /\(1m context\)/i.test(base.displayName)
|
|
119
|
+
? base.displayName
|
|
120
|
+
: `${base.displayName} (1M context)`;
|
|
121
|
+
return {
|
|
122
|
+
...toUnified(base),
|
|
123
|
+
id: `${sdkStem}${ONE_MILLION_SUFFIX}`,
|
|
124
|
+
displayName,
|
|
125
|
+
contextWindow: 1_000_000,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function resolveModel(
|
|
130
|
+
query: string,
|
|
131
|
+
): Promise<UnifiedModelResolution> {
|
|
132
|
+
const exact = resolveExactUnified(query);
|
|
133
|
+
if (exact) {
|
|
134
|
+
return { kind: "exact", model: exact, storedValue: exact.id };
|
|
135
|
+
}
|
|
87
136
|
|
|
88
137
|
// No exact match -- try a substring search across display names and aliases
|
|
89
138
|
const allModels = getModels(PROVIDER_ID);
|
|
@@ -112,9 +161,7 @@ export async function resolveModel(
|
|
|
112
161
|
export async function getModelInfo(
|
|
113
162
|
id: string,
|
|
114
163
|
): Promise<UnifiedModelInfo | undefined> {
|
|
115
|
-
|
|
116
|
-
const model = getModel(canonicalId);
|
|
117
|
-
return model ? toUnified(model) : undefined;
|
|
164
|
+
return resolveExactUnified(id);
|
|
118
165
|
}
|
|
119
166
|
|
|
120
167
|
export async function getSettingsPresentation(
|
|
@@ -293,10 +293,39 @@ export function detectCodexAuth(
|
|
|
293
293
|
* `"not supported when using Codex with a ChatGPT account"`. Match on
|
|
294
294
|
* that substring (case-insensitive, generous on whitespace) so a
|
|
295
295
|
* future wording shift still trips a soft-match.
|
|
296
|
+
*
|
|
297
|
+
* Also matches the 404 "model … does not exist or you do not have
|
|
298
|
+
* access" shape ({@link isCodexModelNotFoundError}): a model retired for
|
|
299
|
+
* the account is the same situation from the caller's point of view.
|
|
296
300
|
*/
|
|
297
301
|
export function isChatGptModelMismatchError(message: string): boolean {
|
|
298
|
-
|
|
299
|
-
|
|
302
|
+
if (
|
|
303
|
+
/not\s+supported\s+when\s+using\s+codex\s+with\s+a\s+chatgpt\s+account/i.test(
|
|
304
|
+
message,
|
|
305
|
+
)
|
|
306
|
+
) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
return isCodexModelNotFoundError(message);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Detect the "model retired / not granted" 404 the ChatGPT Codex endpoint
|
|
314
|
+
* returns once a model is withdrawn from an account:
|
|
315
|
+
*
|
|
316
|
+
* `unexpected status 404 Not Found: The model \`gpt-5.5\` does not exist
|
|
317
|
+
* or you do not have access to it.`
|
|
318
|
+
*
|
|
319
|
+
* Every Codex cron run hit exactly this from 2026-09-24 onward. It is as
|
|
320
|
+
* definitive as the 400 mismatch — the server names the model and says
|
|
321
|
+
* the account can't use it — so it takes the same fallback/learning path.
|
|
322
|
+
* Requires both the 404 status and the "model … does not exist" wording
|
|
323
|
+
* so an unrelated 404 (a missing MCP resource, a bad URL) can't trip it.
|
|
324
|
+
*/
|
|
325
|
+
export function isCodexModelNotFoundError(message: string): boolean {
|
|
326
|
+
return (
|
|
327
|
+
/\b404\b/.test(message) &&
|
|
328
|
+
/\bmodel\b[^\n]{0,120}?\bdoes\s+not\s+exist\b/i.test(message)
|
|
300
329
|
);
|
|
301
330
|
}
|
|
302
331
|
|
|
@@ -37,14 +37,28 @@ export const CODEX_SYSTEM_PROMPT_SUFFIX = codexSystemPromptSuffix("telegram");
|
|
|
37
37
|
export const CODEX_DEFAULT_MODEL = "gpt-5-codex";
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
* via ChatGPT OAuth (`~/.codex/auth.json` `auth_mode: "chatgpt"`).
|
|
42
|
-
* The `gpt-5-codex` model is rejected with a 400
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
40
|
+
* Last-resort default model for the Codex backend when the user is signed
|
|
41
|
+
* in via ChatGPT OAuth (`~/.codex/auth.json` `auth_mode: "chatgpt"`).
|
|
42
|
+
* The `gpt-5-codex` model is rejected with a 400 `invalid_request_error`
|
|
43
|
+
* ("not supported when using Codex with a ChatGPT account") on this auth
|
|
44
|
+
* path.
|
|
45
|
+
*
|
|
46
|
+
* This is only the floor of the resolution ladder — see
|
|
47
|
+
* `getCodexChatGptDefaultModel()` in `models.ts`, which prefers (1) an
|
|
48
|
+
* explicit operator override, then (2) the Codex CLI's own default for the
|
|
49
|
+
* signed-in account (the first listed model in `~/.codex/models_cache.json`
|
|
50
|
+
* by priority). `gpt-6-astra` is the first entry of the model catalog
|
|
51
|
+
* bundled with codex-cli 0.154. The previous value, `gpt-5.5`, was retired
|
|
52
|
+
* for ChatGPT accounts in Sep 2026: every run on it returned
|
|
53
|
+
* `404 The model gpt-5.5 does not exist or you do not have access to it`.
|
|
54
|
+
*/
|
|
55
|
+
export const CODEX_CHATGPT_DEFAULT_MODEL = "gpt-6-astra";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Environment override for the ChatGPT-OAuth default model. Takes
|
|
59
|
+
* precedence over the `codexChatGptDefaultModel` config key.
|
|
46
60
|
*/
|
|
47
|
-
export const
|
|
61
|
+
export const CODEX_CHATGPT_MODEL_ENV = "TALON_CODEX_CHATGPT_MODEL";
|
|
48
62
|
|
|
49
63
|
/**
|
|
50
64
|
* ThreadOptions permission settings shared by both the chat handler and
|
|
@@ -72,6 +72,8 @@ interface CodexCacheModelEntry {
|
|
|
72
72
|
description?: string;
|
|
73
73
|
visibility?: "list" | "hide" | string;
|
|
74
74
|
supported_in_api?: boolean;
|
|
75
|
+
/** Picker order — lower sorts first; the CLI's default is the lowest. */
|
|
76
|
+
priority?: number;
|
|
75
77
|
context_window?: number;
|
|
76
78
|
default_reasoning_level?: string;
|
|
77
79
|
supported_reasoning_levels?: Array<{
|
|
@@ -351,6 +353,33 @@ export async function fetchOpenAiModels(
|
|
|
351
353
|
* Non-existent cache file throws so the caller's catch path logs it
|
|
352
354
|
* at debug level and the picker falls back to curated.
|
|
353
355
|
*/
|
|
356
|
+
/**
|
|
357
|
+
* The Codex CLI's own default for the account: the listed, API-callable
|
|
358
|
+
* entry with the lowest `priority`. First entry wins ties, matching the
|
|
359
|
+
* CLI's stable sort. `null` when nothing qualifies.
|
|
360
|
+
*/
|
|
361
|
+
function cliDefaultModel(
|
|
362
|
+
data: ReadonlyArray<CodexCacheModelEntry | null | undefined>,
|
|
363
|
+
): string | null {
|
|
364
|
+
let best: string | null = null;
|
|
365
|
+
let bestPriority = Number.POSITIVE_INFINITY;
|
|
366
|
+
for (const entry of data) {
|
|
367
|
+
if (!entry || typeof entry.slug !== "string" || !entry.slug) continue;
|
|
368
|
+
if (entry.visibility === "hide" || entry.supported_in_api === false) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const priority =
|
|
372
|
+
typeof entry.priority === "number" && Number.isFinite(entry.priority)
|
|
373
|
+
? entry.priority
|
|
374
|
+
: Number.MAX_SAFE_INTEGER;
|
|
375
|
+
if (priority < bestPriority) {
|
|
376
|
+
bestPriority = priority;
|
|
377
|
+
best = entry.slug;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return best;
|
|
381
|
+
}
|
|
382
|
+
|
|
354
383
|
export async function loadCodexCacheModels(): Promise<void> {
|
|
355
384
|
const path = getCodexCachePath();
|
|
356
385
|
let raw: string;
|
|
@@ -381,6 +410,7 @@ export async function loadCodexCacheModels(): Promise<void> {
|
|
|
381
410
|
const state = getState();
|
|
382
411
|
state.discoveredModels.clear();
|
|
383
412
|
state.discoveredModelMetadata.clear();
|
|
413
|
+
state.discoveredDefaultModel = cliDefaultModel(data);
|
|
384
414
|
let kept = 0;
|
|
385
415
|
let dropped = 0;
|
|
386
416
|
for (const entry of data) {
|
|
@@ -429,6 +459,7 @@ export async function loadCodexCacheModels(): Promise<void> {
|
|
|
429
459
|
const fetchedAt = json.fetched_at ?? "unknown";
|
|
430
460
|
log(
|
|
431
461
|
"agent",
|
|
432
|
-
`Codex: loaded ${kept} models from ${path} (fetched_at=${fetchedAt}, filtered ${dropped} hidden/api-disabled entries
|
|
462
|
+
`Codex: loaded ${kept} models from ${path} (fetched_at=${fetchedAt}, filtered ${dropped} hidden/api-disabled entries` +
|
|
463
|
+
`${state.discoveredDefaultModel ? `, CLI default ${state.discoveredDefaultModel}` : ""})`,
|
|
433
464
|
);
|
|
434
465
|
}
|
|
@@ -28,10 +28,7 @@ import { initCodexAgent, getCodexAuthInfo } from "./init.js";
|
|
|
28
28
|
import { handleMessage as codexHandleMessage } from "./handler/index.js";
|
|
29
29
|
import { runOneShotAgent as codexRunOneShotAgent } from "./one-shot.js";
|
|
30
30
|
import { resetState as resetCodexState } from "./state.js";
|
|
31
|
-
import {
|
|
32
|
-
CODEX_DEFAULT_MODEL,
|
|
33
|
-
CODEX_CHATGPT_DEFAULT_MODEL,
|
|
34
|
-
} from "./constants.js";
|
|
31
|
+
import { CODEX_DEFAULT_MODEL } from "./constants.js";
|
|
35
32
|
import {
|
|
36
33
|
resolveModel as codexResolveModel,
|
|
37
34
|
getModelInfo as codexGetModelInfo,
|
|
@@ -40,6 +37,7 @@ import {
|
|
|
40
37
|
getProviderModels as codexGetProviderModels,
|
|
41
38
|
formatModelError as codexFormatModelError,
|
|
42
39
|
listModels as codexListModels,
|
|
40
|
+
getCodexChatGptDefaultModel,
|
|
43
41
|
} from "./models.js";
|
|
44
42
|
|
|
45
43
|
const codexFactory: BackendFactory = {
|
|
@@ -71,7 +69,7 @@ const codexFactory: BackendFactory = {
|
|
|
71
69
|
getDefaultModelId: () => {
|
|
72
70
|
const auth = getCodexAuthInfo();
|
|
73
71
|
return auth?.mode === "chatgpt"
|
|
74
|
-
?
|
|
72
|
+
? getCodexChatGptDefaultModel()
|
|
75
73
|
: CODEX_DEFAULT_MODEL;
|
|
76
74
|
},
|
|
77
75
|
getRawModelInfo: (id) => codexGetModelInfo(id),
|
|
@@ -45,7 +45,6 @@ import {
|
|
|
45
45
|
import {
|
|
46
46
|
codexSystemPromptSuffix,
|
|
47
47
|
CODEX_DEFAULT_MODEL,
|
|
48
|
-
CODEX_CHATGPT_DEFAULT_MODEL,
|
|
49
48
|
CODEX_THREAD_PERMISSIONS,
|
|
50
49
|
} from "../constants.js";
|
|
51
50
|
import {
|
|
@@ -64,6 +63,7 @@ import {
|
|
|
64
63
|
chatGptFallbackFor,
|
|
65
64
|
getModelInfo,
|
|
66
65
|
isCodexOAuthIncompat,
|
|
66
|
+
getCodexChatGptDefaultModel,
|
|
67
67
|
} from "../models.js";
|
|
68
68
|
import { supportsReasoningLevel } from "../../../core/models/reasoning-levels.js";
|
|
69
69
|
import { toCodexReasoningEffort } from "../effort.js";
|
|
@@ -142,7 +142,7 @@ async function maybeFallbackForChatGptMismatch(
|
|
|
142
142
|
const isOAuth = authInfo?.mode === "chatgpt";
|
|
143
143
|
const silent =
|
|
144
144
|
isOAuth &&
|
|
145
|
-
activeModel !==
|
|
145
|
+
activeModel !== getCodexChatGptDefaultModel() &&
|
|
146
146
|
isSilentOAuthExitError(probeText);
|
|
147
147
|
|
|
148
148
|
if (!explicit && !silent) return undefined;
|
|
@@ -170,7 +170,7 @@ async function maybeFallbackForChatGptMismatch(
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
const fallbackModel =
|
|
173
|
-
chatGptFallbackFor(activeModel) ??
|
|
173
|
+
chatGptFallbackFor(activeModel) ?? getCodexChatGptDefaultModel();
|
|
174
174
|
if (fallbackModel === activeModel) return undefined;
|
|
175
175
|
|
|
176
176
|
// Only EXPLICIT mismatch errors are persisted as OAuth-incompat —
|
|
@@ -227,8 +227,8 @@ function buildTurnPrompt(params: QueryParams): string {
|
|
|
227
227
|
/**
|
|
228
228
|
* Codex accepts arbitrary model strings; we pass through whatever the
|
|
229
229
|
* caller resolved (chat-settings → config) and fall back to the auth-aware
|
|
230
|
-
* default: `gpt-5-codex` when an API key is present,
|
|
231
|
-
* ChatGPT OAuth is configured (because `gpt-5-codex` is rejected with a
|
|
230
|
+
* default: `gpt-5-codex` when an API key is present, the resolved ChatGPT
|
|
231
|
+
* default (`getCodexChatGptDefaultModel`) when only ChatGPT OAuth is configured (because `gpt-5-codex` is rejected with a
|
|
232
232
|
* 400 on ChatGPT-mode accounts). A model known to be OAuth-incompat on a
|
|
233
233
|
* ChatGPT-OAuth account is swapped pre-emptively rather than letting the
|
|
234
234
|
* first turn fail.
|
|
@@ -237,13 +237,13 @@ function resolveCodexModel(chatId: string, requested: string | undefined) {
|
|
|
237
237
|
const authInfo = getCodexAuthInfo();
|
|
238
238
|
const authAwareDefault =
|
|
239
239
|
authInfo?.mode === "chatgpt"
|
|
240
|
-
?
|
|
240
|
+
? getCodexChatGptDefaultModel()
|
|
241
241
|
: CODEX_DEFAULT_MODEL;
|
|
242
242
|
const requestedModel = requested ?? authAwareDefault;
|
|
243
243
|
let activeModel = requestedModel;
|
|
244
244
|
if (authInfo?.mode === "chatgpt" && isCodexOAuthIncompat(requestedModel)) {
|
|
245
245
|
const fallback =
|
|
246
|
-
chatGptFallbackFor(requestedModel) ??
|
|
246
|
+
chatGptFallbackFor(requestedModel) ?? getCodexChatGptDefaultModel();
|
|
247
247
|
// Guard against a learned-but-no-fallback case — only swap when the
|
|
248
248
|
// fallback is actually different from what we'd already run.
|
|
249
249
|
if (fallback !== requestedModel) {
|
|
@@ -37,6 +37,36 @@ import { awaitDiscovery, hasAttemptedDiscovery } from "./discovery.js";
|
|
|
37
37
|
import { getState } from "./state.js";
|
|
38
38
|
import { getCodexAuthInfo } from "./init.js";
|
|
39
39
|
import { isKnownOAuthIncompat } from "./oauth-incompat.js";
|
|
40
|
+
import {
|
|
41
|
+
CODEX_CHATGPT_DEFAULT_MODEL,
|
|
42
|
+
CODEX_CHATGPT_MODEL_ENV,
|
|
43
|
+
} from "./constants.js";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The model a ChatGPT-OAuth Codex session runs when nothing more specific
|
|
47
|
+
* applies — the target of every pre-emptive OAuth swap and mismatch
|
|
48
|
+
* fallback. Resolution order:
|
|
49
|
+
*
|
|
50
|
+
* 1. `TALON_CODEX_CHATGPT_MODEL` env var (operator override);
|
|
51
|
+
* 2. `codexChatGptDefaultModel` in config;
|
|
52
|
+
* 3. the Codex CLI's own default for the signed-in account (first
|
|
53
|
+
* listed model in `~/.codex/models_cache.json` by priority), unless
|
|
54
|
+
* Talon has already learned that id fails on this account;
|
|
55
|
+
* 4. {@link CODEX_CHATGPT_DEFAULT_MODEL}, the bundled floor.
|
|
56
|
+
*
|
|
57
|
+
* Hardcoding a single id is what broke in Sep 2026: OpenAI retired
|
|
58
|
+
* `gpt-5.5` for ChatGPT accounts and every run 404'd until a release.
|
|
59
|
+
*/
|
|
60
|
+
export function getCodexChatGptDefaultModel(): string {
|
|
61
|
+
const env = process.env[CODEX_CHATGPT_MODEL_ENV]?.trim();
|
|
62
|
+
if (env) return env;
|
|
63
|
+
const state = getState();
|
|
64
|
+
const configured = state.config?.codexChatGptDefaultModel?.trim();
|
|
65
|
+
if (configured) return configured;
|
|
66
|
+
const discovered = state.discoveredDefaultModel;
|
|
67
|
+
if (discovered && !isKnownOAuthIncompat(discovered)) return discovered;
|
|
68
|
+
return CODEX_CHATGPT_DEFAULT_MODEL;
|
|
69
|
+
}
|
|
40
70
|
|
|
41
71
|
/**
|
|
42
72
|
* Codex-specific model metadata extension.
|
|
@@ -56,15 +86,26 @@ export interface CodexModelInfo extends UnifiedModelInfo {
|
|
|
56
86
|
* Curated metadata for models we recognise.
|
|
57
87
|
*
|
|
58
88
|
* Order matters: `getSettingsPresentation` lists curated models in
|
|
59
|
-
* this order, and
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
89
|
+
* this order, and the current flagship is intentionally first — the
|
|
90
|
+
* safe default for Talon-on-Codex deployments where the operator hasn't
|
|
91
|
+
* explicitly picked a model. (`gpt-5.5` held that slot until it was
|
|
92
|
+
* retired for ChatGPT accounts in Sep 2026.)
|
|
63
93
|
*
|
|
64
94
|
* Discovered-but-not-curated ids are appended at the end (synthesised
|
|
65
95
|
* with minimal metadata), so the curated entries always render first.
|
|
66
96
|
*/
|
|
67
97
|
export const CODEX_MODELS: CodexModelInfo[] = [
|
|
98
|
+
{
|
|
99
|
+
// Flagship of the catalog bundled with codex-cli 0.154 and the floor
|
|
100
|
+
// of the ChatGPT default ladder (`getCodexChatGptDefaultModel`).
|
|
101
|
+
// No context window here: the account's models_cache.json supplies it.
|
|
102
|
+
id: "gpt-6-astra",
|
|
103
|
+
displayName: "GPT-6 Astra",
|
|
104
|
+
provider: "openai",
|
|
105
|
+
providerName: "OpenAI",
|
|
106
|
+
selectable: true,
|
|
107
|
+
reasoning: true,
|
|
108
|
+
},
|
|
68
109
|
{
|
|
69
110
|
id: "gpt-5.5",
|
|
70
111
|
displayName: "GPT-5.5",
|
|
@@ -149,19 +190,20 @@ export function isCodexOAuthIncompat(id: string): boolean {
|
|
|
149
190
|
*
|
|
150
191
|
* Returns `undefined` when:
|
|
151
192
|
* - The id isn't recognised as OAuth-incompat (caller can skip).
|
|
152
|
-
* - The id IS the
|
|
153
|
-
*
|
|
154
|
-
*
|
|
193
|
+
* - The id IS the resolved ChatGPT default
|
|
194
|
+
* ({@link getCodexChatGptDefaultModel}) itself — no further fallback
|
|
195
|
+
* exists; if even the default fails, the credential is the problem,
|
|
196
|
+
* not the model.
|
|
155
197
|
*
|
|
156
|
-
* For everything else returns `gpt-5
|
|
157
|
-
* default. (The curated table has only `gpt-5-codex` flagged as
|
|
198
|
+
* For everything else returns the resolved ChatGPT default. (The curated table has only `gpt-5-codex` flagged as
|
|
158
199
|
* `apiKeyOnly: true`; runtime-learned entries cover the rest:
|
|
159
200
|
* `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.3-codex`, `gpt-5.2`, etc.)
|
|
160
201
|
*/
|
|
161
202
|
export function chatGptFallbackFor(id: string): string | undefined {
|
|
162
203
|
if (!isCodexOAuthIncompat(id)) return undefined;
|
|
163
|
-
|
|
164
|
-
return
|
|
204
|
+
const fallback = getCodexChatGptDefaultModel();
|
|
205
|
+
if (id === fallback) return undefined;
|
|
206
|
+
return fallback;
|
|
165
207
|
}
|
|
166
208
|
|
|
167
209
|
/**
|