@oh-my-pi/pi-coding-agent 17.2.10 → 17.2.12

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.
Files changed (247) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/{CHANGELOG-rhwzpexa.md → CHANGELOG-k9ghy5sn.md} +53 -0
  3. package/dist/cli.js +10775 -10762
  4. package/dist/types/advisor/runtime.d.ts +1 -1
  5. package/dist/types/capability/mcp.d.ts +11 -0
  6. package/dist/types/capability/skill.d.ts +6 -0
  7. package/dist/types/cli/command-help.d.ts +3 -0
  8. package/dist/types/commands/share.d.ts +25 -0
  9. package/dist/types/commit/agentic/index.d.ts +3 -1
  10. package/dist/types/commit/execute.d.ts +32 -0
  11. package/dist/types/commit/index.d.ts +2 -1
  12. package/dist/types/commit/pipeline.d.ts +7 -1
  13. package/dist/types/config/custom-models.d.ts +31 -0
  14. package/dist/types/config/model-config-values.d.ts +19 -0
  15. package/dist/types/config/model-patch.d.ts +93 -0
  16. package/dist/types/config/model-provider-discovery.d.ts +51 -0
  17. package/dist/types/config/model-registry.d.ts +9 -52
  18. package/dist/types/config/settings-schema.d.ts +1 -34
  19. package/dist/types/config/settings.d.ts +5 -3
  20. package/dist/types/debug/raw-sse-buffer.d.ts +9 -0
  21. package/dist/types/discovery/agent-plugin-format.d.ts +136 -0
  22. package/dist/types/discovery/agent-plugins.d.ts +1 -0
  23. package/dist/types/discovery/contained-path.d.ts +33 -0
  24. package/dist/types/discovery/index.d.ts +1 -0
  25. package/dist/types/eval/executor-base.d.ts +8 -2
  26. package/dist/types/eval/kernel-session-registry.d.ts +60 -0
  27. package/dist/types/export/share.d.ts +1 -1
  28. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  29. package/dist/types/extensibility/extensions/loader.d.ts +7 -0
  30. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  31. package/dist/types/extensibility/shared-events.d.ts +3 -2
  32. package/dist/types/extensibility/skills.d.ts +5 -0
  33. package/dist/types/lsp/diagnostics.d.ts +96 -0
  34. package/dist/types/lsp/index.d.ts +7 -128
  35. package/dist/types/lsp/servers.d.ts +72 -0
  36. package/dist/types/lsp/tool.d.ts +42 -0
  37. package/dist/types/lsp/workspace-diagnostics.d.ts +12 -0
  38. package/dist/types/lsp/writethrough.d.ts +33 -0
  39. package/dist/types/mcp/transports/header-policy.d.ts +46 -0
  40. package/dist/types/mcp/types.d.ts +15 -0
  41. package/dist/types/modes/components/agent-hub-projection.d.ts +2 -7
  42. package/dist/types/modes/components/agent-hub-renderer.d.ts +0 -7
  43. package/dist/types/modes/components/agent-hub.d.ts +2 -0
  44. package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
  45. package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
  46. package/dist/types/modes/components/custom-editor.d.ts +1 -2
  47. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  48. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  49. package/dist/types/modes/theme/color.d.ts +19 -0
  50. package/dist/types/modes/theme/loader.d.ts +19 -0
  51. package/dist/types/modes/theme/schema.d.ts +175 -0
  52. package/dist/types/modes/theme/symbols.d.ts +28 -0
  53. package/dist/types/modes/theme/theme-class.d.ts +244 -0
  54. package/dist/types/modes/theme/theme.d.ts +9 -280
  55. package/dist/types/modes/theme/tui-adapters.d.ts +13 -0
  56. package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
  57. package/dist/types/registry/agent-registry.d.ts +1 -3
  58. package/dist/types/secrets/index.d.ts +26 -2
  59. package/dist/types/secrets/message-transform.d.ts +40 -0
  60. package/dist/types/secrets/obfuscator.d.ts +0 -108
  61. package/dist/types/secrets/placeholder-scan.d.ts +95 -0
  62. package/dist/types/secrets/placeholder.d.ts +94 -0
  63. package/dist/types/secrets/replacement.d.ts +82 -0
  64. package/dist/types/session/agent-session-events.d.ts +2 -2
  65. package/dist/types/session/agent-session-types.d.ts +6 -0
  66. package/dist/types/session/agent-session.d.ts +2 -2
  67. package/dist/types/session/messages.d.ts +1 -0
  68. package/dist/types/session/prewalk.d.ts +3 -1
  69. package/dist/types/session/session-handoff.d.ts +3 -3
  70. package/dist/types/session/session-manager.d.ts +32 -0
  71. package/dist/types/session/session-provider-boundary.d.ts +1 -1
  72. package/dist/types/session/session-tools.d.ts +8 -0
  73. package/dist/types/session/turn-recovery.d.ts +9 -4
  74. package/dist/types/slash-commands/builtin-collaboration.d.ts +2 -0
  75. package/dist/types/slash-commands/builtin-completions.d.ts +36 -0
  76. package/dist/types/slash-commands/builtin-control.d.ts +2 -0
  77. package/dist/types/slash-commands/builtin-lifecycle.d.ts +3 -0
  78. package/dist/types/slash-commands/builtin-marketplace.d.ts +11 -0
  79. package/dist/types/slash-commands/builtin-modes.d.ts +5 -0
  80. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  81. package/dist/types/slash-commands/builtin-session.d.ts +2 -0
  82. package/dist/types/task/index.d.ts +2 -0
  83. package/dist/types/tools/gh-common.d.ts +69 -0
  84. package/dist/types/tools/gh-pr-checkout.d.ts +84 -0
  85. package/dist/types/tools/gh-pr-diff.d.ts +102 -0
  86. package/dist/types/tools/gh-run-watch.d.ts +78 -0
  87. package/dist/types/tools/gh-search.d.ts +61 -0
  88. package/dist/types/tools/gh-types.d.ts +340 -0
  89. package/dist/types/tools/gh-view.d.ts +88 -0
  90. package/dist/types/tools/gh.d.ts +4 -201
  91. package/dist/types/tools/read-archive.d.ts +16 -0
  92. package/dist/types/tools/read-format.d.ts +104 -0
  93. package/dist/types/tools/read-path-resolution.d.ts +19 -0
  94. package/dist/types/tools/read-pdf-images.d.ts +12 -0
  95. package/dist/types/tools/read-renderer.d.ts +24 -0
  96. package/dist/types/tools/read-selector.d.ts +27 -0
  97. package/dist/types/tools/read-sqlite.d.ts +16 -0
  98. package/dist/types/tools/read-summary.d.ts +19 -0
  99. package/dist/types/tools/read.d.ts +1 -23
  100. package/dist/types/tools/shell-tokenize.d.ts +18 -1
  101. package/dist/types/utils/changelog.d.ts +0 -5
  102. package/package.json +13 -13
  103. package/scripts/legacy-pi-virtual-module.ts +4 -1
  104. package/src/advisor/runtime.ts +12 -7
  105. package/src/capability/mcp.ts +11 -0
  106. package/src/capability/skill.ts +6 -0
  107. package/src/cli/command-help.ts +4 -0
  108. package/src/cli/gallery-cli.ts +1 -1
  109. package/src/cli-commands.ts +5 -0
  110. package/src/commands/commit.ts +16 -3
  111. package/src/commands/share.ts +71 -0
  112. package/src/commit/agentic/index.ts +39 -21
  113. package/src/commit/execute.ts +56 -0
  114. package/src/commit/index.ts +2 -1
  115. package/src/commit/pipeline.ts +20 -7
  116. package/src/config/custom-models.ts +188 -0
  117. package/src/config/model-config-values.ts +128 -0
  118. package/src/config/model-discovery.ts +1 -1
  119. package/src/config/model-patch.ts +252 -0
  120. package/src/config/model-provider-discovery.ts +132 -0
  121. package/src/config/model-registry.ts +84 -704
  122. package/src/config/settings-schema.ts +2 -33
  123. package/src/config/settings.ts +53 -3
  124. package/src/debug/raw-sse-buffer.ts +20 -0
  125. package/src/discovery/agent-plugin-format.ts +551 -0
  126. package/src/discovery/agent-plugins.ts +341 -0
  127. package/src/discovery/claude-plugins.ts +21 -5
  128. package/src/discovery/contained-path.ts +75 -0
  129. package/src/discovery/helpers.ts +23 -9
  130. package/src/discovery/index.ts +1 -0
  131. package/src/discovery/omp-plugins.ts +20 -7
  132. package/src/edit/hashline/diff.ts +5 -1
  133. package/src/eval/executor-base.ts +39 -6
  134. package/src/eval/jl/executor.ts +82 -371
  135. package/src/eval/kernel-session-registry.ts +398 -0
  136. package/src/eval/py/executor.ts +53 -261
  137. package/src/eval/rb/executor.ts +36 -279
  138. package/src/exec/non-interactive-env.ts +1 -0
  139. package/src/export/share.ts +2 -1
  140. package/src/extensibility/custom-tools/types.ts +2 -2
  141. package/src/extensibility/extensions/loader.ts +78 -14
  142. package/src/extensibility/extensions/runner.ts +6 -0
  143. package/src/extensibility/extensions/types.ts +12 -0
  144. package/src/extensibility/plugins/marketplace/manager.ts +2 -1
  145. package/src/extensibility/shared-events.ts +3 -2
  146. package/src/extensibility/skills.ts +9 -0
  147. package/src/internal-urls/skill-protocol.ts +14 -0
  148. package/src/internal-urls/vault-protocol.ts +2 -2
  149. package/src/launch/client.ts +11 -1
  150. package/src/launch/protocol.ts +7 -2
  151. package/src/lsp/diagnostics.ts +516 -0
  152. package/src/lsp/index.ts +20 -2819
  153. package/src/lsp/servers.ts +296 -0
  154. package/src/lsp/tool.ts +1352 -0
  155. package/src/lsp/workspace-diagnostics.ts +170 -0
  156. package/src/lsp/writethrough.ts +561 -0
  157. package/src/main.ts +3 -2
  158. package/src/mcp/config.ts +3 -0
  159. package/src/mcp/manager.ts +12 -9
  160. package/src/mcp/transports/header-policy.ts +95 -0
  161. package/src/mcp/transports/http.ts +35 -52
  162. package/src/mcp/transports/sse.ts +20 -27
  163. package/src/mcp/types.ts +15 -0
  164. package/src/modes/acp/acp-agent.ts +15 -4
  165. package/src/modes/components/agent-dashboard.ts +2 -0
  166. package/src/modes/components/agent-hub-projection.ts +2 -2
  167. package/src/modes/components/agent-hub-renderer.ts +3 -7
  168. package/src/modes/components/agent-hub.ts +5 -0
  169. package/src/modes/components/agent-transcript-viewer.ts +3 -0
  170. package/src/modes/components/chat-transcript-builder.ts +3 -0
  171. package/src/modes/components/custom-editor.ts +0 -9
  172. package/src/modes/components/status-line/component.ts +1 -0
  173. package/src/modes/components/status-line/segments.ts +6 -4
  174. package/src/modes/components/status-line/types.ts +2 -0
  175. package/src/modes/components/tool-execution.ts +13 -16
  176. package/src/modes/components/tree-selector.ts +62 -3
  177. package/src/modes/controllers/command-controller.ts +8 -2
  178. package/src/modes/controllers/event-controller.ts +15 -15
  179. package/src/modes/controllers/input-controller.ts +20 -2
  180. package/src/modes/controllers/selector-controller.ts +1 -0
  181. package/src/modes/theme/color.ts +133 -0
  182. package/src/modes/theme/loader.ts +178 -0
  183. package/src/modes/theme/schema.ts +263 -0
  184. package/src/modes/theme/symbols.ts +1000 -0
  185. package/src/modes/theme/theme-class.ts +611 -0
  186. package/src/modes/theme/theme.ts +27 -2453
  187. package/src/modes/theme/tui-adapters.ts +279 -0
  188. package/src/modes/utils/transcript-render-helpers.ts +4 -2
  189. package/src/modes/utils/ui-helpers.ts +1 -0
  190. package/src/registry/agent-registry.ts +2 -2
  191. package/src/sdk.ts +6 -49
  192. package/src/secrets/index.ts +51 -6
  193. package/src/secrets/message-transform.ts +287 -0
  194. package/src/secrets/obfuscator.ts +39 -1303
  195. package/src/secrets/placeholder-scan.ts +506 -0
  196. package/src/secrets/placeholder.ts +309 -0
  197. package/src/secrets/replacement.ts +216 -0
  198. package/src/session/agent-session-events.ts +2 -2
  199. package/src/session/agent-session-types.ts +6 -0
  200. package/src/session/agent-session.ts +146 -21
  201. package/src/session/indexed-session-storage.ts +7 -2
  202. package/src/session/messages.ts +1 -0
  203. package/src/session/prewalk.ts +57 -17
  204. package/src/session/session-advisors.ts +32 -34
  205. package/src/session/session-context.ts +3 -5
  206. package/src/session/session-handoff.ts +39 -16
  207. package/src/session/session-manager.ts +67 -3
  208. package/src/session/session-provider-boundary.ts +3 -6
  209. package/src/session/session-tools.ts +37 -3
  210. package/src/session/todo-tracker.ts +11 -1
  211. package/src/session/turn-recovery.ts +115 -84
  212. package/src/slash-commands/builtin-collaboration.ts +504 -0
  213. package/src/slash-commands/builtin-completions.ts +291 -0
  214. package/src/slash-commands/builtin-control.ts +78 -0
  215. package/src/slash-commands/builtin-lifecycle.ts +506 -0
  216. package/src/slash-commands/builtin-marketplace.ts +567 -0
  217. package/src/slash-commands/builtin-modes.ts +518 -0
  218. package/src/slash-commands/builtin-registry.ts +21 -2996
  219. package/src/slash-commands/builtin-session.ts +592 -0
  220. package/src/task/executor.ts +17 -14
  221. package/src/task/index.ts +21 -6
  222. package/src/tools/bash-skill-urls.ts +15 -0
  223. package/src/tools/bash.ts +10 -11
  224. package/src/tools/debug.ts +1 -1
  225. package/src/tools/gh-common.ts +288 -0
  226. package/src/tools/gh-pr-checkout.ts +679 -0
  227. package/src/tools/gh-pr-diff.ts +473 -0
  228. package/src/tools/gh-run-watch.ts +1015 -0
  229. package/src/tools/gh-search.ts +500 -0
  230. package/src/tools/gh-types.ts +379 -0
  231. package/src/tools/gh-view.ts +612 -0
  232. package/src/tools/gh.ts +109 -3821
  233. package/src/tools/jtd-to-json-schema.ts +123 -21
  234. package/src/tools/read-archive.ts +209 -0
  235. package/src/tools/read-format.ts +593 -0
  236. package/src/tools/read-path-resolution.ts +36 -0
  237. package/src/tools/read-pdf-images.ts +250 -0
  238. package/src/tools/read-renderer.ts +289 -0
  239. package/src/tools/read-selector.ts +84 -0
  240. package/src/tools/read-sqlite.ts +215 -0
  241. package/src/tools/read-summary.ts +199 -0
  242. package/src/tools/read.ts +82 -1824
  243. package/src/tools/shell-tokenize.ts +99 -1
  244. package/src/utils/changelog.ts +1 -1
  245. package/src/utils/title-generator.ts +3 -1
  246. package/src/web/search/providers/exa.ts +1 -1
  247. package/src/web/search/providers/zai.ts +12 -3
@@ -1,6 +1,10 @@
1
- import { execSync } from "node:child_process";
2
1
  import * as path from "node:path";
2
+ import type { ApiKeyResolver, FetchImpl } from "@oh-my-pi/pi-ai";
3
3
  import { registerCustomApi, unregisterCustomApis } from "@oh-my-pi/pi-ai/api-registry";
4
+ import { registerOAuthProvider, unregisterOAuthProvider, unregisterOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
5
+ import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/oauth/types";
6
+ import { setCodexAttestationProvider } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
7
+ import { getProviderDefinition } from "@oh-my-pi/pi-ai/registry";
4
8
  import type {
5
9
  Api,
6
10
  Context,
@@ -12,7 +16,6 @@ import type {
12
16
  } from "@oh-my-pi/pi-ai/types";
13
17
  import type { AssistantMessageEventStream } from "@oh-my-pi/pi-ai/utils/event-stream";
14
18
  import { buildModel } from "@oh-my-pi/pi-catalog/build";
15
- import { isVertexExpressOpenAIUrl } from "@oh-my-pi/pi-catalog/hosts";
16
19
  import { readModelCache } from "@oh-my-pi/pi-catalog/model-cache";
17
20
  import {
18
21
  createModelManager,
@@ -23,63 +26,34 @@ import { getBundledModels, getBundledProviders } from "@oh-my-pi/pi-catalog/mode
23
26
  import {
24
27
  googleAntigravityModelManagerOptions,
25
28
  googleGeminiCliModelManagerOptions,
26
- type OpenAICodexAccount,
27
29
  openaiCodexModelManagerOptions,
28
30
  PROVIDER_DESCRIPTORS,
29
31
  resolveModelCacheProviderId,
30
32
  resolveOllamaModelCacheProviderId,
31
33
  } from "@oh-my-pi/pi-catalog/provider-models";
32
- import {
33
- collapseBuiltModelVariants,
34
- getVariantAliasSources,
35
- resolveVariantAlias,
36
- } from "@oh-my-pi/pi-catalog/variant-collapse";
37
-
38
- const SPECIAL_MODEL_MANAGER_PROVIDER_IDS: readonly string[] = [
39
- "google-antigravity",
40
- "google-gemini-cli",
41
- "openai-codex",
42
- ];
43
-
44
- const STARTUP_MODEL_CACHE_PROVIDER_IDS: readonly string[] = [
45
- ...PROVIDER_DESCRIPTORS.map(descriptor => descriptor.providerId),
46
- ...SPECIAL_MODEL_MANAGER_PROVIDER_IDS,
47
- ];
48
-
49
- // Sentinels for local-only OAuth tokens — declared inline to avoid loading
50
- // provider modules at startup. Must match packages/ai/src/registry/llama-cpp.ts,
51
- // packages/ai/src/registry/lm-studio.ts, and packages/ai/src/registry/vllm.ts.
52
- const LOCAL_PROVIDER_PLACEHOLDERS = new Set<string>(["llama-cpp-local", "lm-studio-local", "vllm-local"]);
53
-
54
- /**
55
- * Hard bound for extension-provided fetchDynamicModels to prevent indefinite hangs
56
- * during runtime provider discovery. Uses a cancellable manual timer (not AbortSignal.timeout)
57
- * so a successful fast path does not leave an armed timeout signal for concurrent GC.
58
- */
59
- const RUNTIME_DYNAMIC_MODEL_FETCH_TIMEOUT_MS = 15_000;
60
- // Built-in discovery preflight mirror of the catalog model-manager's private
61
- // cache timings (model-manager.ts: DEFAULT_CACHE_TTL_MS / NON_AUTHORITATIVE_RETRY_MS).
62
- // Built-in descriptors never override cacheTtlMs, so agreeing with these values
63
- // makes the OAuth-refresh preflight fire exactly when the manager will fetch.
64
- const BUILT_IN_DISCOVERY_CACHE_TTL_MS = 2 * 60 * 60 * 1000;
65
- const BUILT_IN_DISCOVERY_NON_AUTHORITATIVE_RETRY_MS = 5 * 60 * 1000;
66
-
67
- import type { ApiKeyResolver, FetchImpl } from "@oh-my-pi/pi-ai";
68
- import { registerOAuthProvider, unregisterOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
69
- import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/oauth/types";
70
- import { setCodexAttestationProvider } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
71
- import { getProviderDefinition } from "@oh-my-pi/pi-ai/registry";
72
- import {
73
- getBundledModelReferenceIndex,
74
- inheritReferenceThinking,
75
- resolveModelReference,
76
- } from "@oh-my-pi/pi-catalog/identity";
77
- import { $envExact, isBunTestRuntime, isRecord, logger, wrapFetchForExtraCa } from "@oh-my-pi/pi-utils";
78
- import { parseModelString, resolveProviderModelReference } from "../config/model-resolver";
34
+ import { collapseBuiltModelVariants } from "@oh-my-pi/pi-catalog/variant-collapse";
35
+ import { isBunTestRuntime, logger, wrapFetchForExtraCa } from "@oh-my-pi/pi-utils";
36
+ import { resolveProviderModelReference } from "../config/model-resolver";
79
37
  import { generateCodexAttestation } from "../live/attestation";
80
- import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
38
+ import type { AuthStorage } from "../session/auth-storage";
81
39
  import { type ApiKeyResolverModel, type ApiKeyResolverOptions, createApiKeyResolver } from "./api-key-resolver";
82
40
  import type { ConfigError, ConfigFile } from "./config-file";
41
+ import {
42
+ buildCustomModelOverlay,
43
+ type CustomModelDefinitionLike,
44
+ type CustomModelOverlay,
45
+ finalizeCustomModel,
46
+ mergeAuthHeaderSources,
47
+ normalizeSuppressedSelector,
48
+ resolveModelOverrideWithAliases,
49
+ } from "./custom-models";
50
+ import {
51
+ type CommandApiKeyResolution,
52
+ createLiveConfigHeaders,
53
+ isCommandConfigValue,
54
+ resolveConfigHeaders,
55
+ resolveConfigValue,
56
+ } from "./model-config-values";
83
57
  import {
84
58
  applyLlamaCppQwenThinking,
85
59
  DISCOVERY_DEFAULT_MAX_TOKENS,
@@ -91,6 +65,45 @@ import {
91
65
  getOllamaContextLengthOverride,
92
66
  normalizeLiteLLMDiscoveryBaseUrl,
93
67
  } from "./model-discovery";
68
+ import {
69
+ AUTHORITATIVE_RUNTIME_CATALOG_PROVIDERS,
70
+ applyModelOverride,
71
+ applyModelPatch,
72
+ dropProviderModels,
73
+ type ModelPatch,
74
+ mergeByModelKey,
75
+ mergeCompat,
76
+ mergeDiscoveredModel,
77
+ mergeProviderRemoteCompactionConfig,
78
+ mergeRemoteCompactionConfig,
79
+ type ProviderOverride,
80
+ providersWithAuthoritativeProjectCatalog,
81
+ toModelSpec,
82
+ } from "./model-patch";
83
+ import {
84
+ BUILT_IN_DISCOVERY_CACHE_TTL_MS,
85
+ BUILT_IN_DISCOVERY_NON_AUTHORITATIVE_RETRY_MS,
86
+ type BuiltInDiscoveryResult,
87
+ extractGoogleOAuthToken,
88
+ getOAuthCredentialsForProvider,
89
+ isAuthenticated,
90
+ isDiscoveryBearerApiKey,
91
+ kNoAuth,
92
+ type ProviderDiscoveryState,
93
+ RUNTIME_DYNAMIC_MODEL_FETCH_TIMEOUT_MS,
94
+ resolveCodexDiscoveryAccounts,
95
+ STARTUP_MODEL_CACHE_PROVIDER_IDS,
96
+ withRuntimeDynamicModelsTimeout,
97
+ } from "./model-provider-discovery";
98
+
99
+ export { mergeDiscoveredModel } from "./model-patch";
100
+ export {
101
+ isAuthenticated,
102
+ kNoAuth,
103
+ type ProviderDiscoveryState,
104
+ type ProviderDiscoveryStatus,
105
+ } from "./model-provider-discovery";
106
+
94
107
  import { ModelsConfigFile, type ProviderValidationModel, validateProviderConfiguration } from "./models-config";
95
108
  import type { ModelOverride, ModelsConfig, ProviderAuthMode } from "./models-config-schema";
96
109
  import { settings } from "./settings";
@@ -99,180 +112,6 @@ import { settings } from "./settings";
99
112
  // requests; the pi-ai provider resolves it just-in-time per request.
100
113
  setCodexAttestationProvider(generateCodexAttestation);
101
114
 
102
- export const kNoAuth = "N/A";
103
-
104
- export function isAuthenticated(apiKey: string | undefined | null): apiKey is string {
105
- return Boolean(apiKey) && apiKey !== kNoAuth;
106
- }
107
-
108
- function isDiscoveryBearerApiKey(apiKey: string | undefined | null): apiKey is string {
109
- return isAuthenticated(apiKey) && !LOCAL_PROVIDER_PLACEHOLDERS.has(apiKey);
110
- }
111
-
112
- /**
113
- * Wraps an extension-provided fetchDynamicModels call with a hard timeout.
114
- * Uses a cancellable manual timer (not AbortSignal.timeout) so that a fast
115
- * successful path does not leave an armed timeout signal for concurrent GC.
116
- * The inner fetcher does not receive a signal (extension contract has none).
117
- */
118
- async function withRuntimeDynamicModelsTimeout<T>(timeoutMs: number, run: () => Promise<T>): Promise<T> {
119
- const { promise: timeoutPromise, reject: timeoutReject } = Promise.withResolvers<never>();
120
- const timer = setTimeout(() => {
121
- timeoutReject(new Error(`fetchDynamicModels timed out after ${timeoutMs}ms`));
122
- }, timeoutMs);
123
- try {
124
- return await Promise.race([run(), timeoutPromise]);
125
- } finally {
126
- clearTimeout(timer);
127
- }
128
- }
129
-
130
- /** Provider override config (baseUrl, headers, apiKey, compat, transport) without custom models */
131
- interface ProviderOverride {
132
- baseUrl?: string;
133
- headers?: Record<string, string>;
134
- apiKey?: string;
135
- authHeader?: boolean;
136
- compat?: ModelSpec<Api>["compat"];
137
- remoteCompaction?: RemoteCompactionConfig<Api>;
138
- transport?: Model<Api>["transport"];
139
- }
140
-
141
- /**
142
- * Merge a freshly discovered model with the matching bundled/configured entry
143
- * (or a runtime provider override when no bundled entry exists).
144
- *
145
- * `baseUrl` resolution priority:
146
- * 1. User-set `providerOverride.baseUrl` (explicit override in models.json)
147
- * 2. Discovered baseUrl (xiaomi `tp-` token-plan keys resolve to
148
- * `token-plan-sgp.xiaomimimo.com` at discovery time)
149
- * 3. Existing bundled baseUrl (the host baked into `models.json`)
150
- *
151
- * `transport` resolution priority:
152
- * 1. `providerOverride.transport` (e.g. `pi-native` for auth-gateway users)
153
- * 2. `existing.transport` (carried over from boot-time override application)
154
- * 3. `model.transport` (rarely set — discovery defaults omit it)
155
- *
156
- * Without (1), the user's override would lose to discovery; without (2)
157
- * preferred over (3), the bundled `api.xiaomimimo.com` would shadow the
158
- * tp- token-plan host and produce 401s on the first stream call.
159
- * Without explicit transport propagation, an openrouter (or any) entry
160
- * marked `transport: pi-native` in models.yml silently reverts to the
161
- * default openai-completions transport after the background catalog
162
- * refresh — so the first `/model` switch after boot hits the raw OpenAI
163
- * chat-completions URL instead of the gateway's `/v1/pi/stream` (#2555).
164
- * See `xiaomi-tp-discovery-merge.test.ts` and the `refresh()` baseUrl-override
165
- * regression in `model-registry.test.ts`.
166
- */
167
- export function mergeDiscoveredModel<TApi extends Api>(
168
- model: Model<TApi>,
169
- existing: Model<Api> | undefined,
170
- providerOverride?: Pick<ProviderOverride, "baseUrl" | "compat" | "headers" | "remoteCompaction" | "transport">,
171
- ): Model<TApi> {
172
- if (existing) {
173
- const supportsTools = model.supportsTools ?? existing.supportsTools;
174
- return buildModel({
175
- ...model,
176
- baseUrl: providerOverride?.baseUrl ?? model.baseUrl ?? existing.baseUrl,
177
- headers: existing.headers ? { ...existing.headers, ...model.headers } : model.headers,
178
- transport: providerOverride?.transport ?? existing.transport ?? model.transport,
179
- remoteCompaction: mergeProviderRemoteCompactionConfig(
180
- mergeRemoteCompactionConfig(existing.remoteCompaction, model.remoteCompaction),
181
- providerOverride?.remoteCompaction,
182
- ),
183
- ...(supportsTools !== undefined ? { supportsTools } : {}),
184
- compat: mergeCompat(model.compatConfig, providerOverride?.compat),
185
- } as ModelSpec<TApi>);
186
- }
187
- if (providerOverride) {
188
- return buildModel({
189
- ...model,
190
- baseUrl: providerOverride.baseUrl ?? model.baseUrl,
191
- headers: providerOverride.headers ? { ...model.headers, ...providerOverride.headers } : model.headers,
192
- ...(providerOverride.transport !== undefined ? { transport: providerOverride.transport } : {}),
193
- remoteCompaction: mergeProviderRemoteCompactionConfig(
194
- model.remoteCompaction,
195
- providerOverride.remoteCompaction,
196
- ),
197
- compat: mergeCompat(model.compatConfig, providerOverride.compat),
198
- } as ModelSpec<TApi>);
199
- }
200
- return model;
201
- }
202
-
203
- const AUTHORITATIVE_RUNTIME_CATALOG_PROVIDERS = new Set<string>(
204
- PROVIDER_DESCRIPTORS.filter(descriptor => descriptor.dynamicModelsAuthoritative).map(
205
- descriptor => descriptor.providerId,
206
- ),
207
- );
208
-
209
- function isAuthoritativeProjectCatalogModel(model: Model<Api>): boolean {
210
- return (
211
- model.provider === "google-vertex" &&
212
- model.api === "openai-completions" &&
213
- isVertexExpressOpenAIUrl(model.baseUrl)
214
- );
215
- }
216
-
217
- function providersWithAuthoritativeProjectCatalog(models: readonly Model<Api>[]): Set<string> {
218
- const providers = new Set<string>();
219
- for (const model of models) {
220
- if (isAuthoritativeProjectCatalogModel(model)) {
221
- providers.add(model.provider);
222
- }
223
- }
224
- return providers;
225
- }
226
-
227
- function dropProviderModels(models: readonly Model<Api>[], providers: ReadonlySet<string>): Model<Api>[] {
228
- return models.filter(model => !providers.has(model.provider));
229
- }
230
-
231
- /**
232
- * Merge `incoming` entries into a copy of `base`, keyed by `provider`+`id`.
233
- * Matches are replaced with `combine(existing, entry)`; new entries are
234
- * appended as `combine(undefined, entry)`.
235
- */
236
- function mergeByModelKey<T extends { provider: string; id: string }>(
237
- base: readonly Model<Api>[],
238
- incoming: readonly T[],
239
- combine: (existing: Model<Api> | undefined, entry: T) => Model<Api>,
240
- ): Model<Api>[] {
241
- const merged = [...base];
242
- const indexByKey = new Map<string, number>();
243
- for (let i = 0; i < merged.length; i += 1) {
244
- indexByKey.set(`${merged[i].provider}\u0000${merged[i].id}`, i);
245
- }
246
- for (const entry of incoming) {
247
- const key = `${entry.provider}\u0000${entry.id}`;
248
- const existingIndex = indexByKey.get(key);
249
- if (existingIndex !== undefined) {
250
- merged[existingIndex] = combine(merged[existingIndex], entry);
251
- } else {
252
- merged.push(combine(undefined, entry));
253
- indexByKey.set(key, merged.length - 1);
254
- }
255
- }
256
- return merged;
257
- }
258
-
259
- interface BuiltInDiscoveryResult {
260
- models: Model<Api>[];
261
- authoritativeProviders: Set<string>;
262
- }
263
-
264
- export type ProviderDiscoveryStatus = "idle" | "ok" | "empty" | "cached" | "unavailable" | "unauthenticated";
265
-
266
- export interface ProviderDiscoveryState {
267
- provider: string;
268
- status: ProviderDiscoveryStatus;
269
- optional: boolean;
270
- stale: boolean;
271
- fetchedAt?: number;
272
- models: string[];
273
- error?: string;
274
- }
275
-
276
115
  /** Result of loading custom models config. */
277
116
  interface CustomModelsResult {
278
117
  models?: CustomModelOverlay[];
@@ -285,491 +124,12 @@ interface CustomModelsResult {
285
124
  found: boolean;
286
125
  }
287
126
 
288
- const commandValueCache = new Map<string, string>();
289
- // Failed `!command` resolutions (non-zero exit, empty stdout) are negative-cached
290
- // with a TTL instead of forever: a transient failure (locked password manager,
291
- // network hiccup) must not disable the key until process restart, but re-running
292
- // the command on every resolution would restore the execSync storm this cache
293
- // exists to prevent. One probe per TTL window bounds both.
294
- const COMMAND_FAILURE_RETRY_MS = 30_000;
295
- const commandFailureRetryAt = new Map<string, number>();
296
-
297
- function isCommandConfigValue(valueConfig: string | undefined): valueConfig is string {
298
- return valueConfig?.startsWith("!") === true;
299
- }
300
-
301
- function resolveCommandConfig(command: string): string | undefined {
302
- const cached = commandValueCache.get(command);
303
- if (cached !== undefined) return cached;
304
- const retryAt = commandFailureRetryAt.get(command);
305
- if (retryAt !== undefined && Date.now() < retryAt) return undefined;
306
- try {
307
- const stdout = execSync(command, { encoding: "utf8", timeout: 10_000, windowsHide: true });
308
- const trimmed = stdout.trim();
309
- if (trimmed.length === 0) {
310
- commandFailureRetryAt.set(command, Date.now() + COMMAND_FAILURE_RETRY_MS);
311
- return undefined;
312
- }
313
- commandFailureRetryAt.delete(command);
314
- commandValueCache.set(command, trimmed);
315
- return trimmed;
316
- } catch {
317
- commandFailureRetryAt.set(command, Date.now() + COMMAND_FAILURE_RETRY_MS);
318
- return undefined;
319
- }
320
- }
321
-
322
- interface CommandApiKeyResolution {
323
- configured: boolean;
324
- value?: string;
325
- }
326
- /**
327
- * Resolve a models.yml/models.yaml secret/config value to an actual value.
328
- * `!cmd` runs a shell command and returns trimmed stdout, otherwise env vars are
329
- * checked first and the input falls back to a literal value.
330
- */
331
- function resolveConfigValue(valueConfig: string): string | undefined {
332
- if (valueConfig.startsWith("!")) return resolveCommandConfig(valueConfig.slice(1).trim());
333
- const envValue = $envExact(valueConfig);
334
- if (envValue) return envValue;
335
- return valueConfig;
336
- }
337
-
338
- type HeaderSource = Record<string, string> | undefined;
339
-
340
- interface HeaderResolutionOptions {
341
- authHeader?: boolean;
342
- apiKeyConfig?: string;
343
- }
344
-
345
- function materializeConfigHeaderSources(
346
- sources: readonly HeaderSource[],
347
- options?: HeaderResolutionOptions,
348
- ): Record<string, string> | undefined {
349
- const resolved: Record<string, string> = {};
350
- for (const source of sources) {
351
- if (!source) continue;
352
- for (const [key, value] of Object.entries(source)) {
353
- const next = resolveConfigValue(value);
354
- if (next) resolved[key] = next;
355
- }
356
- }
357
- if (options?.authHeader && options.apiKeyConfig) {
358
- const resolvedKey = resolveConfigValue(options.apiKeyConfig);
359
- if (resolvedKey) resolved.Authorization = `Bearer ${resolvedKey}`;
360
- }
361
- return Object.keys(resolved).length > 0 ? resolved : undefined;
362
- }
363
-
364
- function createLiveConfigHeaders(
365
- sources: readonly HeaderSource[],
366
- options?: HeaderResolutionOptions,
367
- ): Record<string, string> | undefined {
368
- const liveSources = sources.filter((source): source is Record<string, string> => source !== undefined);
369
- if (liveSources.length === 0 && (!options?.authHeader || !options.apiKeyConfig)) return undefined;
370
-
371
- const localHeaders: Record<string, string> = {};
372
- const allSources = [...liveSources, localHeaders];
373
- const current = () => materializeConfigHeaderSources(allSources, options) ?? {};
374
- return new Proxy(localHeaders, {
375
- get(target, property, receiver) {
376
- if (typeof property !== "string") return Reflect.get(target, property, receiver);
377
- return current()[property];
378
- },
379
- set(target, property, value) {
380
- if (typeof property !== "string" || typeof value !== "string") return false;
381
- target[property] = value;
382
- return true;
383
- },
384
- deleteProperty(target, property) {
385
- if (typeof property !== "string") return false;
386
- delete target[property];
387
- return true;
388
- },
389
- has(_target, property) {
390
- if (typeof property !== "string") return false;
391
- return Object.hasOwn(current(), property);
392
- },
393
- ownKeys() {
394
- return Reflect.ownKeys(current());
395
- },
396
- getOwnPropertyDescriptor(_target, property) {
397
- if (typeof property !== "string") return undefined;
398
- const headers = current();
399
- if (!Object.hasOwn(headers, property)) return undefined;
400
- return {
401
- configurable: true,
402
- enumerable: true,
403
- value: headers[property],
404
- writable: true,
405
- };
406
- },
407
- });
408
- }
409
-
410
- function resolveConfigHeaders(headers: Record<string, string> | undefined): Record<string, string> | undefined {
411
- return materializeConfigHeaderSources([headers]);
412
- }
413
-
414
- function extractGoogleOAuthToken(value: string | undefined): string | undefined {
415
- if (!isAuthenticated(value)) return undefined;
416
- try {
417
- const parsed = JSON.parse(value) as { token?: unknown };
418
- if (Object.hasOwn(parsed, "token")) {
419
- if (typeof parsed.token !== "string") {
420
- return undefined;
421
- }
422
- const token = parsed.token.trim();
423
- return token.length > 0 ? token : undefined;
424
- }
425
- } catch {
426
- // OAuth values for Google providers are expected to be JSON, but custom setups may already provide raw token.
427
- }
428
- return value;
429
- }
430
-
431
- function getOAuthCredentialsForProvider(authStorage: AuthStorage, provider: string): OAuthCredential[] {
432
- const providerEntry = authStorage.getAll()[provider];
433
- if (!providerEntry) {
434
- return [];
435
- }
436
- const entries = Array.isArray(providerEntry) ? providerEntry : [providerEntry];
437
- return entries.filter((entry): entry is OAuthCredential => entry.type === "oauth");
438
- }
439
-
440
- /**
441
- * Resolve every configured Codex OAuth account for catalog discovery, refreshing
442
- * each credential exactly once. Codex `/models` is account-scoped, so discovery
443
- * must fetch per account and union the results; resolving a single access token
444
- * (as before) hid models available only through a sibling account (#6265).
445
- *
446
- * Returns `null` when any stored account fails to resolve (e.g. a transient
447
- * refresh failure): the Codex manager is authoritative, so unioning only the
448
- * accounts that resolved would cache a partial catalog and hide the failed
449
- * account's models for the cache TTL. Aborting keeps the previous/bundled
450
- * catalog instead.
451
- */
452
- async function resolveCodexDiscoveryAccounts(
453
- authStorage: AuthStorage,
454
- resolvedAccessToken: string,
455
- ): Promise<OpenAICodexAccount[] | null> {
456
- const accesses = await authStorage.getOAuthAccesses("openai-codex");
457
- const accounts: OpenAICodexAccount[] = [];
458
- for (const access of accesses) {
459
- if (!access.ok) return null;
460
- accounts.push({ accessToken: access.accessToken, accountId: access.accountId });
461
- }
462
- if (!accounts.some(account => account.accessToken === resolvedAccessToken)) {
463
- const matchingCredential = getOAuthCredentialsForProvider(authStorage, "openai-codex").find(
464
- credential => credential.access === resolvedAccessToken,
465
- );
466
- accounts.push({ accessToken: resolvedAccessToken, accountId: matchingCredential?.accountId });
467
- }
468
- return accounts;
469
- }
470
-
471
- function mergeCompat<TBase extends object, TOverride extends object>(
472
- baseCompat: TBase | null | undefined,
473
- overrideCompat: TOverride | null | undefined,
474
- ): (TBase & TOverride) | TBase | TOverride | undefined {
475
- if (!baseCompat) return overrideCompat ?? undefined;
476
- if (!overrideCompat) return baseCompat;
477
-
478
- const merged: Record<string, unknown> = { ...(baseCompat as Record<string, unknown>) };
479
- for (const [key, overrideValue] of Object.entries(overrideCompat)) {
480
- const baseValue = (baseCompat as Record<string, unknown>)[key];
481
- merged[key] =
482
- isRecord(baseValue) && isRecord(overrideValue) ? mergeCompat(baseValue, overrideValue) : overrideValue;
483
- }
484
- return merged as TBase & TOverride;
485
- }
486
-
487
- function mergeRemoteCompactionConfig(
488
- baseConfig: RemoteCompactionConfig<Api> | undefined,
489
- overrideConfig: RemoteCompactionConfig<Api> | undefined,
490
- ): RemoteCompactionConfig<Api> | undefined {
491
- if (!baseConfig) return overrideConfig;
492
- if (!overrideConfig) return baseConfig;
493
- return { ...baseConfig, ...overrideConfig };
494
- }
495
-
496
- function mergeProviderRemoteCompactionConfig(
497
- modelConfig: RemoteCompactionConfig<Api> | undefined,
498
- providerConfig: RemoteCompactionConfig<Api> | undefined,
499
- ): RemoteCompactionConfig<Api> | undefined {
500
- return mergeRemoteCompactionConfig(providerConfig, modelConfig);
501
- }
502
-
503
- /**
504
- * Project a built model back to spec shape for the model-manager/cache
505
- * boundary: sparse compat comes from `compatConfig`, never from the resolved
506
- * record.
507
- */
508
- function toModelSpec<TApi extends Api>(model: Model<TApi>): ModelSpec<TApi> {
509
- return { ...model, compat: model.compatConfig } as ModelSpec<TApi>;
510
- }
511
-
512
- /**
513
- * The patchable subset of `Model` fields shared by `modelOverrides` entries,
514
- * custom model definitions, and parsed custom-model overlays. `undefined`
515
- * always means "leave the base value alone".
516
- */
517
- interface ModelPatch {
518
- name?: string;
519
- reasoning?: boolean;
520
- thinking?: ThinkingConfig;
521
- input?: ("text" | "image")[];
522
- supportsTools?: boolean;
523
- cost?: Partial<Model<Api>["cost"]>;
524
- contextWindow?: number;
525
- maxTokens?: number;
526
- omitMaxOutputTokens?: boolean;
527
- headers?: Record<string, string>;
528
- compat?: ModelSpec<Api>["compat"];
529
- contextPromotionTarget?: string;
530
- compactionModel?: string;
531
- remoteCompaction?: RemoteCompactionConfig<Api>;
532
- premiumMultiplier?: number;
533
- }
534
-
535
- /**
536
- * How a patch treats the base model's transport metadata (headers/compat):
537
- * - `merge`: fold the patch into the base's (modelOverrides semantics).
538
- * - `replace`: the patch owns transport wholesale — same-id custom definitions
539
- * already folded provider-level headers/compat in during parsing, so bundled
540
- * transport metadata must not be re-merged (see `#mergeCustomModels`).
541
- */
542
- type ModelTransportPolicy = "merge" | "replace";
543
-
544
127
  /**
545
128
  * Credential-aware model projection supplied by an extension provider. Receives
546
129
  * the fully composed catalog and returns the list the host should serve.
547
130
  */
548
131
  type ModifyModelsHook = (models: Model<Api>[], credentials: OAuthCredentials) => Model<Api>[];
549
132
 
550
- function applyModelPatch(base: Model<Api>, patch: ModelPatch, transport: ModelTransportPolicy): Model<Api> {
551
- const result = { ...base };
552
- if (patch.name !== undefined) result.name = patch.name;
553
- if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
554
- if (patch.thinking !== undefined) result.thinking = patch.thinking;
555
- if (patch.input !== undefined) result.input = patch.input;
556
- if (patch.supportsTools !== undefined) result.supportsTools = patch.supportsTools;
557
- if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
558
- if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
559
- if (patch.omitMaxOutputTokens !== undefined) result.omitMaxOutputTokens = patch.omitMaxOutputTokens;
560
- if (patch.contextPromotionTarget !== undefined) result.contextPromotionTarget = patch.contextPromotionTarget;
561
- if (patch.compactionModel !== undefined) result.compactionModel = patch.compactionModel;
562
- if (patch.remoteCompaction !== undefined) {
563
- result.remoteCompaction = mergeRemoteCompactionConfig(base.remoteCompaction, patch.remoteCompaction);
564
- }
565
- if (patch.premiumMultiplier !== undefined) result.premiumMultiplier = patch.premiumMultiplier;
566
- if (patch.cost) {
567
- result.cost = {
568
- input: patch.cost.input ?? base.cost.input,
569
- output: patch.cost.output ?? base.cost.output,
570
- cacheRead: patch.cost.cacheRead ?? base.cost.cacheRead,
571
- cacheWrite: patch.cost.cacheWrite ?? base.cost.cacheWrite,
572
- };
573
- }
574
- let compat: ModelSpec<Api>["compat"];
575
- if (transport === "merge") {
576
- if (patch.headers) {
577
- result.headers = { ...base.headers, ...patch.headers };
578
- }
579
- compat = mergeCompat(base.compatConfig, patch.compat);
580
- } else {
581
- result.headers = patch.headers;
582
- compat = patch.compat;
583
- }
584
- const built = buildModel({ ...result, compat } as ModelSpec<Api>);
585
- if (patch.thinking !== undefined && built.thinking !== undefined) {
586
- // Config-authored capability metadata owns the explicit surface; build
587
- // first so non-reasoning and wire-disabled models still suppress it.
588
- built.thinking = patch.thinking;
589
- }
590
- return built;
591
- }
592
-
593
- function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {
594
- return applyModelPatch(model, override as ModelPatch, "merge");
595
- }
596
-
597
- interface CustomModelDefinitionLike extends ModelPatch {
598
- id: string;
599
- api?: Api;
600
- baseUrl?: string;
601
- cost?: Model<Api>["cost"];
602
- }
603
-
604
- interface CustomModelBuildOptions {
605
- useDefaults: boolean;
606
- }
607
-
608
- interface CustomModelOverlay extends ModelPatch {
609
- id: string;
610
- provider: string;
611
- api: Api;
612
- baseUrl: string;
613
- cost?: Model<Api>["cost"];
614
- isOAuth?: boolean;
615
- }
616
-
617
- function mergeCustomModelHeaders(
618
- providerHeaders: Record<string, string> | undefined,
619
- modelHeaders: Record<string, string> | undefined,
620
- authHeader: boolean | undefined,
621
- apiKeyConfig: string | undefined,
622
- ): Record<string, string> | undefined {
623
- return createLiveConfigHeaders([providerHeaders, modelHeaders], { authHeader, apiKeyConfig });
624
- }
625
-
626
- function mergeAuthHeaderSources(
627
- sources: readonly HeaderSource[],
628
- authHeader: boolean | undefined,
629
- apiKeyConfig: string | undefined,
630
- ): Record<string, string> | undefined {
631
- return createLiveConfigHeaders(sources, { authHeader, apiKeyConfig });
632
- }
633
-
634
- /**
635
- * Decide whether a custom-yaml model should force OAuth-style request shaping.
636
- * - Explicit `auth: oauth` → force on.
637
- * endpoints are typically Claude-Code-style proxies (e.g. CLIProxyAPI) that expect
638
- * the cloaked request shape regardless of how the proxy itself is authenticated.
639
- * - Otherwise → unset.
640
- */
641
- function resolveCustomModelIsOAuth(api: Api, providerAuth: ProviderAuthMode | undefined): boolean | undefined {
642
- if (providerAuth === "oauth") return true;
643
- if (providerAuth !== undefined) return undefined;
644
- if (api === "anthropic-messages") return true;
645
- return undefined;
646
- }
647
-
648
- function buildCustomModelOverlay(
649
- providerName: string,
650
- providerBaseUrl: string,
651
- providerApi: Api | undefined,
652
- providerHeaders: Record<string, string> | undefined,
653
- providerApiKey: string | undefined,
654
- authHeader: boolean | undefined,
655
- providerCompat: ModelSpec<Api>["compat"] | undefined,
656
- providerAuth: ProviderAuthMode | undefined,
657
- providerRemoteCompaction: RemoteCompactionConfig<Api> | undefined,
658
- modelDef: CustomModelDefinitionLike,
659
- ): CustomModelOverlay | undefined {
660
- const api = modelDef.api ?? providerApi;
661
- if (!api) return undefined;
662
- return {
663
- id: modelDef.id,
664
- provider: providerName,
665
- api,
666
- baseUrl: modelDef.baseUrl ?? providerBaseUrl,
667
- name: modelDef.name,
668
- reasoning: modelDef.reasoning,
669
- thinking: modelDef.thinking,
670
- input: modelDef.input,
671
- supportsTools: modelDef.supportsTools,
672
- cost: modelDef.cost,
673
- contextWindow: modelDef.contextWindow,
674
- maxTokens: modelDef.maxTokens,
675
- omitMaxOutputTokens: modelDef.omitMaxOutputTokens,
676
- headers: mergeCustomModelHeaders(providerHeaders, modelDef.headers, authHeader, providerApiKey),
677
- compat: mergeCompat(providerCompat, modelDef.compat),
678
- contextPromotionTarget: modelDef.contextPromotionTarget,
679
- compactionModel: modelDef.compactionModel,
680
- remoteCompaction: mergeRemoteCompactionConfig(providerRemoteCompaction, modelDef.remoteCompaction),
681
- premiumMultiplier: modelDef.premiumMultiplier,
682
- isOAuth: resolveCustomModelIsOAuth(api, providerAuth),
683
- };
684
- }
685
-
686
- function applyStandaloneCustomModelPolicies(model: CustomModelOverlay): CustomModelOverlay {
687
- if (model.id !== "gpt-5.4" || model.provider === "github-copilot" || model.contextWindow !== undefined) {
688
- return model;
689
- }
690
- return { ...model, contextWindow: 1_000_000 };
691
- }
692
-
693
- function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuildOptions): Model<Api> {
694
- const resolvedModel = options.useDefaults ? applyStandaloneCustomModelPolicies(model) : model;
695
- const reference = options.useDefaults
696
- ? resolveModelReference(resolvedModel.id, getBundledModelReferenceIndex())
697
- : undefined;
698
- const cost =
699
- resolvedModel.cost ??
700
- reference?.cost ??
701
- (options.useDefaults ? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } : undefined);
702
- const input = resolvedModel.input ?? reference?.input ?? (options.useDefaults ? ["text"] : undefined);
703
- const supportsTools = resolvedModel.supportsTools ?? reference?.supportsTools;
704
- return buildModel({
705
- id: resolvedModel.id,
706
- name: resolvedModel.name ?? (options.useDefaults ? resolvedModel.id : undefined),
707
- api: resolvedModel.api,
708
- provider: resolvedModel.provider,
709
- baseUrl: resolvedModel.baseUrl,
710
- reasoning: resolvedModel.reasoning ?? reference?.reasoning ?? (options.useDefaults ? false : undefined),
711
- thinking: inheritReferenceThinking(resolvedModel.thinking, reference, resolvedModel.provider),
712
- input: input as ("text" | "image")[],
713
- ...(supportsTools !== undefined ? { supportsTools } : {}),
714
- cost,
715
- contextWindow: resolvedModel.contextWindow ?? reference?.contextWindow ?? (options.useDefaults ? 128000 : null),
716
- maxTokens: resolvedModel.maxTokens ?? reference?.maxTokens ?? (options.useDefaults ? 16384 : null),
717
- headers: resolvedModel.headers,
718
- omitMaxOutputTokens: resolvedModel.omitMaxOutputTokens ?? reference?.omitMaxOutputTokens,
719
- compat: mergeCompat(reference?.compatConfig, resolvedModel.compat),
720
- contextPromotionTarget: resolvedModel.contextPromotionTarget,
721
- compactionModel: resolvedModel.compactionModel,
722
- remoteCompaction: resolvedModel.remoteCompaction,
723
- premiumMultiplier: resolvedModel.premiumMultiplier,
724
- isOAuth: resolvedModel.isOAuth,
725
- } as ModelSpec<Api>);
726
- }
727
-
728
- function normalizeSuppressedSelector(
729
- selector: string,
730
- hasLiveModel?: (provider: string, id: string) => boolean,
731
- ): string {
732
- const trimmed = selector.trim();
733
- if (!trimmed) return trimmed;
734
- const parsed = parseModelString(trimmed, {
735
- allowMaxSuffix: true,
736
- allowAutoAlias: true,
737
- isLiteralModelId: (provider, id) => hasLiveModel?.(provider, id) === true,
738
- });
739
- if (!parsed) return trimmed;
740
- // Retired effort-tier variant ids normalize to their collapsed logical id
741
- // so persisted suppressions keyed by raw member ids still bind.
742
- const aliasId = resolveVariantAlias(parsed.provider, parsed.id);
743
- return `${parsed.provider}/${aliasId ?? parsed.id}`;
744
- }
745
-
746
- /**
747
- * Look up a model's override, falling back to entries keyed by retired
748
- * effort-tier variant ids (models.yml authored before collapsing). A raw key
749
- * only re-binds when no live model holds that id.
750
- */
751
- function resolveModelOverrideWithAliases(
752
- overrides: Map<string, ModelOverride>,
753
- model: Model<Api>,
754
- hasLiveModel: (provider: string, id: string) => boolean,
755
- ): ModelOverride | undefined {
756
- const direct = overrides.get(model.id);
757
- if (direct) return direct;
758
- for (const rawId of getVariantAliasSources(model.provider, model.id)) {
759
- if (hasLiveModel(model.provider, rawId)) continue;
760
- const remapped = overrides.get(rawId);
761
- if (remapped) {
762
- logger.debug("model override re-keyed through variant alias", {
763
- provider: model.provider,
764
- from: rawId,
765
- to: model.id,
766
- });
767
- return remapped;
768
- }
769
- }
770
- return undefined;
771
- }
772
-
773
133
  function getDisabledProviderIdsFromSettings(): Set<string> {
774
134
  try {
775
135
  return new Set(settings.get("disabledProviders"));
@@ -2514,6 +1874,26 @@ export class ModelRegistry {
2514
1874
  this.#reloadStaticModels();
2515
1875
  }
2516
1876
 
1877
+ /**
1878
+ * Remove one extension-registered provider and restore its static models.
1879
+ */
1880
+ unregisterProvider(providerName: string): void {
1881
+ const sourceId = this.#runtimeProviderSourceByName.get(providerName);
1882
+ if (sourceId) {
1883
+ const sourceProviders = this.#runtimeProvidersBySource.get(sourceId);
1884
+ sourceProviders?.delete(providerName);
1885
+ if (sourceProviders?.size === 0) {
1886
+ this.#runtimeProvidersBySource.delete(sourceId);
1887
+ }
1888
+ this.#runtimeProviderSourceByName.delete(providerName);
1889
+ }
1890
+ unregisterOAuthProvider(providerName);
1891
+ this.#ensureFullSnapshot();
1892
+ this.#clearRuntimeProviderState(providerName);
1893
+ this.#lastStaticLoadMtime = null;
1894
+ this.#reloadStaticModels();
1895
+ }
1896
+
2517
1897
  /**
2518
1898
  * Remove registrations for extension sources that are no longer active.
2519
1899
  */