jeopi-cli 16.5.1 → 16.5.6

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.
@@ -1,6 +1,7 @@
1
1
  export declare const BUILTIN_TOOL_NAMES: readonly ["read", "bash", "edit", "ast_grep", "ast_edit", "ask", "debug", "eval", "ssh", "github", "glob", "grep", "lsp", "inspect_image", "vision_verify", "browser", "checkpoint", "rewind", "task", "job", "irc", "subagent", "todo", "web_search", "search_tool_bm25", "write", "memory_edit", "retain", "recall", "reflect", "learn", "manage_skill"];
2
2
  export type BuiltinToolName = (typeof BUILTIN_TOOL_NAMES)[number];
3
- /** Return the canonical tool name for current and legacy built-in tool IDs. */
3
+ export declare const HIDDEN_TOOL_NAMES: readonly ["yield", "report_finding", "report_tool_issue", "resolve", "goal"];
4
+ /** Canonicalize built-in IDs and legacy aliases. Leave plugin names unchanged. */
4
5
  export declare function normalizeToolName(name: string): string;
5
6
  /** Normalize and deduplicate tool names while preserving first-seen order. */
6
7
  export declare function normalizeToolNames(names: Iterable<string>): string[];
@@ -17,8 +17,8 @@ export interface CodexSearchParams {
17
17
  * Default-model behavior:
18
18
  * - If `PI_CODEX_WEB_SEARCH_MODEL` is set, use it exactly once and surface any
19
19
  * upstream error verbatim.
20
- * - Otherwise prefer ChatGPT-account-safe bundled defaults (GPT-5.4, GPT-5
21
- * Codex, GPT-5, …) and retry the next candidate only when Codex returns the
20
+ * - Otherwise prefer known bundled defaults, then include newly cataloged GPT
21
+ * models, and retry the next candidate only when Codex returns the
22
22
  * known 400 "model is not supported" family. This avoids selecting
23
23
  * `gpt-5-codex-mini` first on ChatGPT accounts, which OpenAI rejects.
24
24
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "jeopi-cli",
4
- "version": "16.5.1",
4
+ "version": "16.5.6",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/akillness/jeopi",
7
7
  "author": "Can Boluk",
@@ -57,17 +57,17 @@
57
57
  "@agentclientprotocol/sdk": "0.25.0",
58
58
  "@babel/parser": "^7.29.7",
59
59
  "@mozilla/readability": "^0.6.0",
60
- "jeopi-hashline": "16.5.1",
61
- "jeopi-stats": "16.5.1",
62
- "jeopi-agent-core": "16.5.1",
63
- "jeopi-ai": "16.5.1",
64
- "jeopi-catalog": "16.5.1",
65
- "jeopi-mnemopi": "16.5.1",
66
- "jeopi-natives": "16.5.1",
67
- "jeopi-tui": "16.5.1",
68
- "jeopi-utils": "16.5.1",
69
- "jeopi-wire": "16.5.1",
70
- "jeopi-snapcompact": "16.5.1",
60
+ "jeopi-hashline": "16.5.6",
61
+ "jeopi-stats": "16.5.6",
62
+ "jeopi-agent-core": "16.5.6",
63
+ "jeopi-ai": "16.5.6",
64
+ "jeopi-catalog": "16.5.6",
65
+ "jeopi-mnemopi": "16.5.6",
66
+ "jeopi-natives": "16.5.6",
67
+ "jeopi-tui": "16.5.6",
68
+ "jeopi-utils": "16.5.6",
69
+ "jeopi-wire": "16.5.6",
70
+ "jeopi-snapcompact": "16.5.6",
71
71
  "@opentelemetry/api": "^1.9.1",
72
72
  "@opentelemetry/context-async-hooks": "^2.7.1",
73
73
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -14,8 +14,13 @@ import type { AssistantMessageEventStream } from "jeopi-ai/utils/event-stream";
14
14
  import { buildModel } from "jeopi-catalog/build";
15
15
  import { isVertexExpressOpenAIUrl } from "jeopi-catalog/hosts";
16
16
  import { readModelCache } from "jeopi-catalog/model-cache";
17
- import { createModelManager, type ModelManagerOptions, type ModelRefreshStrategy } from "jeopi-catalog/model-manager";
18
- import { getBundledModels, getBundledProviders } from "jeopi-catalog/models";
17
+ import {
18
+ createModelManager,
19
+ type ModelManagerOptions,
20
+ type ModelRefreshStrategy,
21
+ restoreCachedModelHeaders,
22
+ } from "jeopi-catalog/model-manager";
23
+ import { type GeneratedProvider, getBundledModels, getBundledProviders } from "jeopi-catalog/models";
19
24
  import {
20
25
  googleAntigravityModelManagerOptions,
21
26
  googleGeminiCliModelManagerOptions,
@@ -940,6 +945,9 @@ export class ModelRegistry {
940
945
  this.#addImplicitDiscoverableProviders(configuredProviders);
941
946
  let builtInModels = this.#applyHardcodedModelPolicies(this.#loadBuiltInModels(overrides));
942
947
  const cachedStandardResult = this.#loadCachedStandardProviderModels();
948
+ builtInModels = builtInModels.filter(
949
+ model => !cachedStandardResult.unresolvedModelIdsByProvider.get(model.provider)?.has(model.id),
950
+ );
943
951
  const cachedStandardModels = this.#applyHardcodedModelPolicies(cachedStandardResult.models);
944
952
  const cachedDiscoveries = this.#applyHardcodedModelPolicies(this.#loadCachedDiscoverableModels());
945
953
  // Only drop bundled fallback models when the cached project-catalog row is
@@ -1028,6 +1036,24 @@ export class ModelRegistry {
1028
1036
  }
1029
1037
 
1030
1038
  #resolveStartupModelCacheProviderId(providerId: string): string {
1039
+ if (providerId === "openai" || providerId === "openai-codex") {
1040
+ const commandKey = this.#resolveCommandBackedApiKey(providerId);
1041
+ const apiKey = commandKey.configured ? commandKey.value : this.authStorage.getModelCacheIdentity(providerId);
1042
+ const baseUrl =
1043
+ this.#runtimeProviderOverrides.get(providerId)?.baseUrl ??
1044
+ this.#providerOverrides.get(providerId)?.baseUrl ??
1045
+ this.getProviderBaseUrl(providerId);
1046
+ if (providerId === "openai-codex") {
1047
+ return (
1048
+ openaiCodexModelManagerOptions({
1049
+ accessToken: apiKey,
1050
+ baseUrl,
1051
+ }).cacheProviderId ?? providerId
1052
+ );
1053
+ }
1054
+ const descriptor = PROVIDER_DESCRIPTORS.find(candidate => candidate.providerId === providerId);
1055
+ return descriptor?.createModelManagerOptions({ apiKey, baseUrl }).cacheProviderId ?? providerId;
1056
+ }
1031
1057
  const descriptor = PROVIDER_DESCRIPTORS.find(candidate => candidate.providerId === providerId);
1032
1058
  if (!descriptor) {
1033
1059
  return providerId;
@@ -1039,10 +1065,22 @@ export class ModelRegistry {
1039
1065
  return descriptor.createModelManagerOptions({ baseUrl, fetch: this.#fetch }).cacheProviderId ?? providerId;
1040
1066
  }
1041
1067
 
1042
- #loadCachedStandardProviderModels(): { models: Model<Api>[]; authoritativeFreshProviders: Set<string> } {
1068
+ #restoreConfiguredModelHeaders(model: Model<Api>): Record<string, string> | undefined {
1069
+ const override = this.#providerOverrides.get(model.provider);
1070
+ const withoutHeaders = { ...model, headers: undefined };
1071
+ const configured = override ? this.#applyProviderTransportOverride(withoutHeaders, override) : withoutHeaders;
1072
+ return this.#applyProviderModelOverrides(model.provider, [configured])[0]?.headers;
1073
+ }
1074
+
1075
+ #loadCachedStandardProviderModels(): {
1076
+ models: Model<Api>[];
1077
+ authoritativeFreshProviders: Set<string>;
1078
+ unresolvedModelIdsByProvider: Map<string, ReadonlySet<string>>;
1079
+ } {
1043
1080
  const configuredDiscoveryProviders = new Set(this.#discoverableProviders.map(provider => provider.provider));
1044
1081
  const cachedModels: Model<Api>[] = [];
1045
1082
  const authoritativeFreshProviders = new Set<string>();
1083
+ const unresolvedModelIdsByProvider = new Map<string, ReadonlySet<string>>();
1046
1084
  for (const providerId of STARTUP_MODEL_CACHE_PROVIDER_IDS) {
1047
1085
  if (configuredDiscoveryProviders.has(providerId)) {
1048
1086
  continue;
@@ -1052,12 +1090,24 @@ export class ModelRegistry {
1052
1090
  if (!cache) {
1053
1091
  continue;
1054
1092
  }
1055
- if (cache.fresh && cache.authoritative) {
1093
+ const descriptor = PROVIDER_DESCRIPTORS.find(candidate => candidate.providerId === providerId);
1094
+ const restored = restoreCachedModelHeaders(
1095
+ cache.models,
1096
+ getBundledModels(providerId as GeneratedProvider),
1097
+ cache.headerOmittedModelIds,
1098
+ cache.unrestorableHeaderModelIds,
1099
+ descriptor?.createModelManagerOptions({}).restorableHeaderFallback,
1100
+ model => this.#restoreConfiguredModelHeaders(model),
1101
+ );
1102
+ if (restored.unresolvedModelIds.size > 0) {
1103
+ unresolvedModelIdsByProvider.set(providerId, restored.unresolvedModelIds);
1104
+ }
1105
+ if (cache.fresh && cache.authoritative && restored.unresolvedModelIds.size === 0) {
1056
1106
  authoritativeFreshProviders.add(providerId);
1057
1107
  }
1058
- const models = cache.models.map(model =>
1059
- model.provider === providerId ? model : { ...model, provider: providerId },
1060
- );
1108
+ const models = restored.models
1109
+ .filter(model => !restored.unresolvedModelIds.has(model.id))
1110
+ .map(model => (model.provider === providerId ? model : { ...model, provider: providerId }));
1061
1111
  const providerOverride = this.#providerOverrides.get(providerId);
1062
1112
  const withTransport = providerOverride
1063
1113
  ? models.map(model => this.#applyProviderTransportOverride(model, providerOverride))
@@ -1072,7 +1122,7 @@ export class ModelRegistry {
1072
1122
  : withTransport.map(model => buildModel(model));
1073
1123
  cachedModels.push(...this.#applyProviderModelOverrides(providerId, withCompat));
1074
1124
  }
1075
- return { models: cachedModels, authoritativeFreshProviders };
1125
+ return { models: cachedModels, authoritativeFreshProviders, unresolvedModelIdsByProvider };
1076
1126
  }
1077
1127
 
1078
1128
  #loadCachedDiscoverableModels(): Model<Api>[] {
@@ -1094,6 +1144,14 @@ export class ModelRegistry {
1094
1144
  });
1095
1145
  continue;
1096
1146
  }
1147
+ const restored = restoreCachedModelHeaders(
1148
+ cache.models,
1149
+ [],
1150
+ cache.headerOmittedModelIds,
1151
+ cache.unrestorableHeaderModelIds,
1152
+ undefined,
1153
+ model => this.#restoreConfiguredModelHeaders(model),
1154
+ );
1097
1155
  const configStale = this.#isDiscoveryCacheOlderThanModelsConfig(cache.updatedAt);
1098
1156
  const models = this.#applyProviderModelOverrides(
1099
1157
  providerConfig.provider,
@@ -1101,7 +1159,7 @@ export class ModelRegistry {
1101
1159
  providerConfig,
1102
1160
  this.#applyProviderCompat(
1103
1161
  providerConfig.compat,
1104
- cache.models.map(model => buildModel(model)),
1162
+ restored.models.filter(model => !restored.unresolvedModelIds.has(model.id)),
1105
1163
  ),
1106
1164
  ),
1107
1165
  );
@@ -1110,7 +1168,12 @@ export class ModelRegistry {
1110
1168
  provider: providerConfig.provider,
1111
1169
  status: "cached",
1112
1170
  optional: providerConfig.optional ?? false,
1113
- stale: providerConfig.discovery.type === "llama.cpp" || !cache.fresh || !cache.authoritative || configStale,
1171
+ stale:
1172
+ providerConfig.discovery.type === "llama.cpp" ||
1173
+ !cache.fresh ||
1174
+ !cache.authoritative ||
1175
+ configStale ||
1176
+ restored.unresolvedModelIds.size > 0,
1114
1177
  fetchedAt: cache.updatedAt,
1115
1178
  models: models.map(model => model.id),
1116
1179
  });
@@ -1126,14 +1189,18 @@ export class ModelRegistry {
1126
1189
  }
1127
1190
 
1128
1191
  #normalizeDiscoverableModels(providerConfig: DiscoveryProviderConfig, models: Model<Api>[]): Model<Api>[] {
1192
+ const override = this.#providerOverrides.get(providerConfig.provider);
1193
+ const withTransport = override
1194
+ ? models.map(model => this.#applyProviderTransportOverride(model, override))
1195
+ : models;
1129
1196
  const withDecoderMetadata =
1130
1197
  providerConfig.discovery.type === "ollama" ||
1131
1198
  providerConfig.discovery.type === "llama.cpp" ||
1132
1199
  providerConfig.discovery.type === "lm-studio"
1133
- ? models.map(model =>
1200
+ ? withTransport.map(model =>
1134
1201
  buildModel({ ...model, imageInputDecoder: "stb", compat: model.compatConfig } as ModelSpec<Api>),
1135
1202
  )
1136
- : models;
1203
+ : withTransport;
1137
1204
 
1138
1205
  const withRemoteCompaction = providerConfig.remoteCompaction
1139
1206
  ? withDecoderMetadata.map(model =>
@@ -1412,6 +1479,15 @@ export class ModelRegistry {
1412
1479
  ): Promise<Model<Api>[]> {
1413
1480
  const cacheProviderId = this.#configuredDiscoveryCacheProviderId(providerConfig);
1414
1481
  const cached = readModelCache<Api>(cacheProviderId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
1482
+ const restored = restoreCachedModelHeaders(
1483
+ cached?.models ?? [],
1484
+ [],
1485
+ cached?.headerOmittedModelIds ?? [],
1486
+ cached?.unrestorableHeaderModelIds ?? [],
1487
+ undefined,
1488
+ model => this.#restoreConfiguredModelHeaders(model),
1489
+ );
1490
+ const usableCachedModels = restored.models.filter(model => !restored.unresolvedModelIds.has(model.id));
1415
1491
  const cacheOlderThanConfig = cached !== null && this.#isDiscoveryCacheOlderThanModelsConfig(cached.updatedAt);
1416
1492
  const shouldForceOnlineDiscovery =
1417
1493
  strategy === "online-if-uncached" && (cacheOlderThanConfig || isAuthoritativeLocalDiscovery(providerConfig));
@@ -1426,15 +1502,10 @@ export class ModelRegistry {
1426
1502
  optional: providerConfig.optional ?? false,
1427
1503
  stale: cached !== null,
1428
1504
  fetchedAt: cached?.updatedAt,
1429
- models: cached?.models.map(model => model.id) ?? [],
1505
+ models: usableCachedModels.map(model => model.id),
1430
1506
  });
1431
1507
  this.#lastDiscoveryWarnings.delete(providerConfig.provider);
1432
- return cached
1433
- ? this.#normalizeDiscoverableModels(
1434
- providerConfig,
1435
- cached.models.map(model => buildModel(model)),
1436
- )
1437
- : [];
1508
+ return cached ? this.#normalizeDiscoverableModels(providerConfig, usableCachedModels) : [];
1438
1509
  }
1439
1510
  }
1440
1511
 
@@ -1444,7 +1515,10 @@ export class ModelRegistry {
1444
1515
  try {
1445
1516
  const models = this.#applyProviderModelOverrides(
1446
1517
  providerId,
1447
- await discoverModelsByProviderType(providerConfig, this.#discoveryContext()),
1518
+ this.#normalizeDiscoverableModels(
1519
+ providerConfig,
1520
+ await discoverModelsByProviderType(providerConfig, this.#discoveryContext()),
1521
+ ),
1448
1522
  );
1449
1523
  this.#lastDiscoveryWarnings.delete(providerId);
1450
1524
  return models.map(toModelSpec);
@@ -1461,6 +1535,7 @@ export class ModelRegistry {
1461
1535
  cacheProviderId,
1462
1536
  cacheTtlMs: 24 * 60 * 60 * 1000,
1463
1537
  fetchDynamicModels,
1538
+ restoreCachedHeaders: model => this.#restoreConfiguredModelHeaders(model),
1464
1539
  });
1465
1540
  const result = await manager.refresh(effectiveStrategy);
1466
1541
  const status = discoveryError
@@ -1594,6 +1669,8 @@ export class ModelRegistry {
1594
1669
  return openaiCodexModelManagerOptions({
1595
1670
  accessToken,
1596
1671
  accountId,
1672
+ baseUrl: this.getProviderBaseUrl("openai-codex"),
1673
+ fetch: this.#fetch,
1597
1674
  });
1598
1675
  },
1599
1676
  },
@@ -1645,6 +1722,11 @@ export class ModelRegistry {
1645
1722
  }
1646
1723
  options.push(descriptor.createOptions(key));
1647
1724
  }
1725
+ for (const option of options) {
1726
+ if (option.providerId === "openai" || option.providerId === "openai-codex") {
1727
+ option.cacheProviderId = this.#resolveStartupModelCacheProviderId(option.providerId);
1728
+ }
1729
+ }
1648
1730
  // Append runtime model managers registered by extensions via fetchDynamicModels.
1649
1731
  for (const { options: managerOpts } of this.#runtimeModelManagers.values()) {
1650
1732
  options.push(managerOpts);
@@ -726,7 +726,7 @@ export class AssistantMessageComponent extends Container {
726
726
  );
727
727
 
728
728
  // Fast path: reuse Markdown children when shape is stable during streaming
729
- if (this.#tryFastPathUpdate(message)) return;
729
+ if (this.#tryFastPathUpdate(message, opts)) return;
730
730
 
731
731
  // Clear content container
732
732
  this.#contentContainer.clear();
@@ -215,6 +215,7 @@ export class StreamingRevealController {
215
215
  #component: StreamingRevealComponent | undefined;
216
216
  #timer: NodeJS.Timeout | undefined;
217
217
  #revealed = 0;
218
+ #targetDirty = false;
218
219
  #hideThinkingBlock = false;
219
220
  #proseOnlyThinking = true;
220
221
  #smoothStreaming = true;
@@ -275,6 +276,9 @@ export class StreamingRevealController {
275
276
  if (!this.#component) return;
276
277
  if (!this.#smoothStreaming) {
277
278
  const total = this.#visibleUnits(message);
279
+ this.#revealed = total;
280
+ this.#targetDirty = false;
281
+ this.#stopTimer();
278
282
  this.#component.updateContent(this.#build(message, total), { transient: true });
279
283
  return;
280
284
  }
@@ -283,6 +287,7 @@ export class StreamingRevealController {
283
287
  // A tool call is a transcript-order boundary: finish any leading
284
288
  // assistant text before EventController renders the separate tool card.
285
289
  this.#revealed = total;
290
+ this.#targetDirty = false;
286
291
  this.#stopTimer();
287
292
  this.#component.updateContent(this.#build(message, this.#revealed), {
288
293
  transient: true,
@@ -292,8 +297,15 @@ export class StreamingRevealController {
292
297
  if (this.#revealed > total) {
293
298
  this.#revealed = total;
294
299
  }
295
- this.#renderCurrent();
296
- this.#syncTimer(total);
300
+ if (this.#revealed < total) {
301
+ // ponytail: reuse the reveal timer; only add another scheduler if streaming needs a separate cadence.
302
+ this.#targetDirty = false;
303
+ this.#syncTimer(total);
304
+ return;
305
+ }
306
+ // Caught-up rewrites still need one render of the newest target at the next tick.
307
+ this.#startTimer();
308
+ this.#targetDirty = true;
297
309
  }
298
310
 
299
311
  stop(): void {
@@ -301,6 +313,7 @@ export class StreamingRevealController {
301
313
  this.#target = undefined;
302
314
  this.#component = undefined;
303
315
  this.#revealed = 0;
316
+ this.#targetDirty = false;
304
317
  this.#unitCounter.reset();
305
318
  }
306
319
 
@@ -377,6 +390,12 @@ export class StreamingRevealController {
377
390
  }
378
391
  const total = this.#visibleUnits(target);
379
392
  if (this.#revealed >= total) {
393
+ if (this.#targetDirty) {
394
+ this.#targetDirty = false;
395
+ this.#revealed = total;
396
+ this.#renderCurrent();
397
+ this.#requestRender();
398
+ }
380
399
  this.#stopTimer();
381
400
  return;
382
401
  }
@@ -59,9 +59,14 @@ function shouldExternalizeImagePayload(
59
59
  return (key === TEXT_CONTENT_KEY && isImageBlock(value)) || key === "images";
60
60
  }
61
61
 
62
+ /** Signature and encrypted fields bind their carrier to its exact bytes. */
63
+ function isNonEmptyString(value: unknown): value is string {
64
+ return typeof value === "string" && value.length > 0;
65
+ }
66
+
62
67
  /**
63
68
  * Recursively truncate large strings in an object for session persistence.
64
- * - Truncates any oversized string fields (key-agnostic)
69
+ * - Truncates oversized strings except signed/encrypted blocks and signature keys
65
70
  * - Externalizes oversized image payloads to blob refs
66
71
  * - Updates lineCount when content is truncated
67
72
  * - Returns original object if no changes needed (structural sharing)
@@ -76,16 +81,29 @@ function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?: string
76
81
  if (shouldExternalizeImagePayload(obj, key)) {
77
82
  return { ...obj, data: externalizeImageDataSync(blobStore, obj.data, obj.mimeType) };
78
83
  }
84
+ // Signed content and encrypted reasoning must replay byte-for-byte. Keep the
85
+ // whole carrier, including paired text and tool arguments; unsigned content
86
+ // still follows the normal size and transient-field sanitization rules.
87
+ if (typeof obj === "object" && "type" in obj) {
88
+ const signed =
89
+ (obj.type === "thinking" && "thinkingSignature" in obj && isNonEmptyString(obj.thinkingSignature)) ||
90
+ (obj.type === "text" && "textSignature" in obj && isNonEmptyString(obj.textSignature)) ||
91
+ (obj.type === "toolCall" && "thoughtSignature" in obj && isNonEmptyString(obj.thoughtSignature));
92
+ const redacted = obj.type === "redactedThinking" && "data" in obj && isNonEmptyString(obj.data);
93
+ const encryptedReasoning =
94
+ obj.type === "reasoning" && "encrypted_content" in obj && isNonEmptyString(obj.encrypted_content);
95
+ if (signed || redacted || encryptedReasoning) return obj;
96
+ }
79
97
 
80
98
  if (typeof obj === "string") {
81
99
  if (key === "image_url" && isImageDataUrl(obj)) {
82
100
  return externalizeImageDataUrlSync(blobStore, obj);
83
101
  }
84
102
  if (obj.length > MAX_PERSIST_CHARS) {
85
- // Cryptographic signatures must be preserved exactly or cleared entirely — never truncated.
86
- // Truncation would produce an invalid signature that the API rejects.
103
+ // Unknown carrier shapes must retain opaque signatures too: clearing or
104
+ // truncating one loses provider context or makes replay invalid.
87
105
  if (key === "thinkingSignature" || key === "thoughtSignature" || key === "textSignature") {
88
- return "";
106
+ return obj;
89
107
  }
90
108
  const limit = Math.max(0, MAX_PERSIST_CHARS - TRUNCATION_NOTICE.length);
91
109
  return `${truncateString(obj, limit)}${TRUNCATION_NOTICE}`;
@@ -35,15 +35,24 @@ export const BUILTIN_TOOL_NAMES = [
35
35
 
36
36
  export type BuiltinToolName = (typeof BUILTIN_TOOL_NAMES)[number];
37
37
 
38
+ export const HIDDEN_TOOL_NAMES = ["yield", "report_finding", "report_tool_issue", "resolve", "goal"] as const;
39
+
38
40
  const LEGACY_BUILTIN_TOOL_NAME_ALIASES: ReadonlyMap<string, BuiltinToolName> = new Map([
39
41
  ["search", "grep"],
40
42
  ["find", "glob"],
41
43
  ]);
42
44
 
43
- /** Return the canonical tool name for current and legacy built-in tool IDs. */
45
+ const CANONICAL_TOOL_NAMES: Record<string, true> = Object.fromEntries(
46
+ [...BUILTIN_TOOL_NAMES, ...HIDDEN_TOOL_NAMES].map(name => [name, true]),
47
+ );
48
+
49
+ /** Canonicalize built-in IDs and legacy aliases. Leave plugin names unchanged. */
44
50
  export function normalizeToolName(name: string): string {
45
51
  const normalized = name.toLowerCase();
46
- return LEGACY_BUILTIN_TOOL_NAME_ALIASES.get(normalized) ?? normalized;
52
+ return (
53
+ LEGACY_BUILTIN_TOOL_NAME_ALIASES.get(normalized) ??
54
+ (Object.hasOwn(CANONICAL_TOOL_NAMES, normalized) ? normalized : name)
55
+ );
47
56
  }
48
57
 
49
58
  /** Normalize and deduplicate tool names while preserving first-seen order. */
@@ -40,7 +40,7 @@ import { AstEditTool } from "./ast-edit";
40
40
  import { AstGrepTool } from "./ast-grep";
41
41
  import { BashTool } from "./bash";
42
42
  import { BrowserTool } from "./browser";
43
- import { type BuiltinToolName, normalizeToolNames } from "./builtin-names";
43
+ import { type BuiltinToolName, type HIDDEN_TOOL_NAMES, normalizeToolNames } from "./builtin-names";
44
44
  import { type CheckpointState, CheckpointTool, type CompletedRewindState, RewindTool } from "./checkpoint";
45
45
  import { DebugTool } from "./debug";
46
46
  import { EvalTool } from "./eval";
@@ -494,7 +494,7 @@ export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
494
494
  report_tool_issue: s => createReportToolIssueTool(s),
495
495
  resolve: s => new ResolveTool(s),
496
496
  goal: s => new GoalTool(s),
497
- };
497
+ } satisfies Record<(typeof HIDDEN_TOOL_NAMES)[number], ToolFactory>;
498
498
 
499
499
  export type ToolName = BuiltinToolName;
500
500
 
@@ -44,17 +44,12 @@ function getDefaultModelCandidates(): string[] {
44
44
  const bundledModels = getBundledModels("openai-codex");
45
45
  const bundledIds = new Set(bundledModels.map(model => model.id));
46
46
  const candidates = DEFAULT_MODEL_PREFERENCES.filter(modelId => bundledIds.has(modelId));
47
-
48
- if (candidates.length > 0) {
49
- return candidates;
50
- }
51
-
52
- const nonMini = bundledModels.find(model => !model.id.includes("mini") && !model.id.includes("spark"));
53
- if (nonMini) {
54
- return [nonMini.id];
47
+ for (const model of bundledModels) {
48
+ if (/^gpt-\d/.test(model.id) && !candidates.includes(model.id)) {
49
+ candidates.push(model.id);
50
+ }
55
51
  }
56
-
57
- return bundledModels[0]?.id ? [bundledModels[0].id] : [FALLBACK_MODEL];
52
+ return candidates.length > 0 ? candidates : [FALLBACK_MODEL];
58
53
  }
59
54
 
60
55
  function shouldRetryWithNextDefaultModel(error: unknown): boolean {
@@ -489,8 +484,8 @@ async function callCodexSearch(
489
484
  * Default-model behavior:
490
485
  * - If `PI_CODEX_WEB_SEARCH_MODEL` is set, use it exactly once and surface any
491
486
  * upstream error verbatim.
492
- * - Otherwise prefer ChatGPT-account-safe bundled defaults (GPT-5.4, GPT-5
493
- * Codex, GPT-5, …) and retry the next candidate only when Codex returns the
487
+ * - Otherwise prefer known bundled defaults, then include newly cataloged GPT
488
+ * models, and retry the next candidate only when Codex returns the
494
489
  * known 400 "model is not supported" family. This avoids selecting
495
490
  * `gpt-5-codex-mini` first on ChatGPT accounts, which OpenAI rejects.
496
491
  */