@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
@@ -0,0 +1,252 @@
1
+ import type { Api, Model, ModelSpec, RemoteCompactionConfig, ThinkingConfig } from "@oh-my-pi/pi-ai/types";
2
+ import { buildModel } from "@oh-my-pi/pi-catalog/build";
3
+ import { isVertexExpressOpenAIUrl } from "@oh-my-pi/pi-catalog/hosts";
4
+ import { PROVIDER_DESCRIPTORS } from "@oh-my-pi/pi-catalog/provider-models";
5
+ import { isRecord } from "@oh-my-pi/pi-utils";
6
+ import type { ModelOverride } from "./models-config-schema";
7
+ /** Provider override config (baseUrl, headers, apiKey, compat, transport) without custom models */
8
+ export interface ProviderOverride {
9
+ baseUrl?: string;
10
+ headers?: Record<string, string>;
11
+ apiKey?: string;
12
+ authHeader?: boolean;
13
+ compat?: ModelSpec<Api>["compat"];
14
+ remoteCompaction?: RemoteCompactionConfig<Api>;
15
+ transport?: Model<Api>["transport"];
16
+ }
17
+
18
+ /**
19
+ * Merge a freshly discovered model with the matching bundled/configured entry
20
+ * (or a runtime provider override when no bundled entry exists).
21
+ *
22
+ * `baseUrl` resolution priority:
23
+ * 1. User-set `providerOverride.baseUrl` (explicit override in models.json)
24
+ * 2. Discovered baseUrl (xiaomi `tp-` token-plan keys resolve to
25
+ * `token-plan-sgp.xiaomimimo.com` at discovery time)
26
+ * 3. Existing bundled baseUrl (the host baked into `models.json`)
27
+ *
28
+ * `transport` resolution priority:
29
+ * 1. `providerOverride.transport` (e.g. `pi-native` for auth-gateway users)
30
+ * 2. `existing.transport` (carried over from boot-time override application)
31
+ * 3. `model.transport` (rarely set — discovery defaults omit it)
32
+ *
33
+ * Without (1), the user's override would lose to discovery; without (2)
34
+ * preferred over (3), the bundled `api.xiaomimimo.com` would shadow the
35
+ * tp- token-plan host and produce 401s on the first stream call.
36
+ * Without explicit transport propagation, an openrouter (or any) entry
37
+ * marked `transport: pi-native` in models.yml silently reverts to the
38
+ * default openai-completions transport after the background catalog
39
+ * refresh — so the first `/model` switch after boot hits the raw OpenAI
40
+ * chat-completions URL instead of the gateway's `/v1/pi/stream` (#2555).
41
+ * See `xiaomi-tp-discovery-merge.test.ts` and the `refresh()` baseUrl-override
42
+ * regression in `model-registry.test.ts`.
43
+ */
44
+ export function mergeDiscoveredModel<TApi extends Api>(
45
+ model: Model<TApi>,
46
+ existing: Model<Api> | undefined,
47
+ providerOverride?: Pick<ProviderOverride, "baseUrl" | "compat" | "headers" | "remoteCompaction" | "transport">,
48
+ ): Model<TApi> {
49
+ if (existing) {
50
+ const supportsTools = model.supportsTools ?? existing.supportsTools;
51
+ return buildModel({
52
+ ...model,
53
+ baseUrl: providerOverride?.baseUrl ?? model.baseUrl ?? existing.baseUrl,
54
+ headers: existing.headers ? { ...existing.headers, ...model.headers } : model.headers,
55
+ transport: providerOverride?.transport ?? existing.transport ?? model.transport,
56
+ remoteCompaction: mergeProviderRemoteCompactionConfig(
57
+ mergeRemoteCompactionConfig(existing.remoteCompaction, model.remoteCompaction),
58
+ providerOverride?.remoteCompaction,
59
+ ),
60
+ ...(supportsTools !== undefined ? { supportsTools } : {}),
61
+ compat: mergeCompat(model.compatConfig, providerOverride?.compat),
62
+ } as ModelSpec<TApi>);
63
+ }
64
+ if (providerOverride) {
65
+ return buildModel({
66
+ ...model,
67
+ baseUrl: providerOverride.baseUrl ?? model.baseUrl,
68
+ headers: providerOverride.headers ? { ...model.headers, ...providerOverride.headers } : model.headers,
69
+ ...(providerOverride.transport !== undefined ? { transport: providerOverride.transport } : {}),
70
+ remoteCompaction: mergeProviderRemoteCompactionConfig(
71
+ model.remoteCompaction,
72
+ providerOverride.remoteCompaction,
73
+ ),
74
+ compat: mergeCompat(model.compatConfig, providerOverride.compat),
75
+ } as ModelSpec<TApi>);
76
+ }
77
+ return model;
78
+ }
79
+
80
+ export const AUTHORITATIVE_RUNTIME_CATALOG_PROVIDERS = new Set<string>(
81
+ PROVIDER_DESCRIPTORS.filter(descriptor => descriptor.dynamicModelsAuthoritative).map(
82
+ descriptor => descriptor.providerId,
83
+ ),
84
+ );
85
+
86
+ function isAuthoritativeProjectCatalogModel(model: Model<Api>): boolean {
87
+ return (
88
+ model.provider === "google-vertex" &&
89
+ model.api === "openai-completions" &&
90
+ isVertexExpressOpenAIUrl(model.baseUrl)
91
+ );
92
+ }
93
+
94
+ export function providersWithAuthoritativeProjectCatalog(models: readonly Model<Api>[]): Set<string> {
95
+ const providers = new Set<string>();
96
+ for (const model of models) {
97
+ if (isAuthoritativeProjectCatalogModel(model)) {
98
+ providers.add(model.provider);
99
+ }
100
+ }
101
+ return providers;
102
+ }
103
+
104
+ export function dropProviderModels(models: readonly Model<Api>[], providers: ReadonlySet<string>): Model<Api>[] {
105
+ return models.filter(model => !providers.has(model.provider));
106
+ }
107
+
108
+ /**
109
+ * Merge `incoming` entries into a copy of `base`, keyed by `provider`+`id`.
110
+ * Matches are replaced with `combine(existing, entry)`; new entries are
111
+ * appended as `combine(undefined, entry)`.
112
+ */
113
+ export function mergeByModelKey<T extends { provider: string; id: string }>(
114
+ base: readonly Model<Api>[],
115
+ incoming: readonly T[],
116
+ combine: (existing: Model<Api> | undefined, entry: T) => Model<Api>,
117
+ ): Model<Api>[] {
118
+ const merged = [...base];
119
+ const indexByKey = new Map<string, number>();
120
+ for (let i = 0; i < merged.length; i += 1) {
121
+ indexByKey.set(`${merged[i].provider}\u0000${merged[i].id}`, i);
122
+ }
123
+ for (const entry of incoming) {
124
+ const key = `${entry.provider}\u0000${entry.id}`;
125
+ const existingIndex = indexByKey.get(key);
126
+ if (existingIndex !== undefined) {
127
+ merged[existingIndex] = combine(merged[existingIndex], entry);
128
+ } else {
129
+ merged.push(combine(undefined, entry));
130
+ indexByKey.set(key, merged.length - 1);
131
+ }
132
+ }
133
+ return merged;
134
+ }
135
+ export function mergeCompat<TBase extends object, TOverride extends object>(
136
+ baseCompat: TBase | null | undefined,
137
+ overrideCompat: TOverride | null | undefined,
138
+ ): (TBase & TOverride) | TBase | TOverride | undefined {
139
+ if (!baseCompat) return overrideCompat ?? undefined;
140
+ if (!overrideCompat) return baseCompat;
141
+
142
+ const merged: Record<string, unknown> = { ...(baseCompat as Record<string, unknown>) };
143
+ for (const [key, overrideValue] of Object.entries(overrideCompat)) {
144
+ const baseValue = (baseCompat as Record<string, unknown>)[key];
145
+ merged[key] =
146
+ isRecord(baseValue) && isRecord(overrideValue) ? mergeCompat(baseValue, overrideValue) : overrideValue;
147
+ }
148
+ return merged as TBase & TOverride;
149
+ }
150
+
151
+ export function mergeRemoteCompactionConfig(
152
+ baseConfig: RemoteCompactionConfig<Api> | undefined,
153
+ overrideConfig: RemoteCompactionConfig<Api> | undefined,
154
+ ): RemoteCompactionConfig<Api> | undefined {
155
+ if (!baseConfig) return overrideConfig;
156
+ if (!overrideConfig) return baseConfig;
157
+ return { ...baseConfig, ...overrideConfig };
158
+ }
159
+
160
+ export function mergeProviderRemoteCompactionConfig(
161
+ modelConfig: RemoteCompactionConfig<Api> | undefined,
162
+ providerConfig: RemoteCompactionConfig<Api> | undefined,
163
+ ): RemoteCompactionConfig<Api> | undefined {
164
+ return mergeRemoteCompactionConfig(providerConfig, modelConfig);
165
+ }
166
+
167
+ /**
168
+ * Project a built model back to spec shape for the model-manager/cache
169
+ * boundary: sparse compat comes from `compatConfig`, never from the resolved
170
+ * record.
171
+ */
172
+ export function toModelSpec<TApi extends Api>(model: Model<TApi>): ModelSpec<TApi> {
173
+ return { ...model, compat: model.compatConfig } as ModelSpec<TApi>;
174
+ }
175
+
176
+ /**
177
+ * The patchable subset of `Model` fields shared by `modelOverrides` entries,
178
+ * custom model definitions, and parsed custom-model overlays. `undefined`
179
+ * always means "leave the base value alone".
180
+ */
181
+ export interface ModelPatch {
182
+ name?: string;
183
+ reasoning?: boolean;
184
+ thinking?: ThinkingConfig;
185
+ input?: ("text" | "image")[];
186
+ supportsTools?: boolean;
187
+ cost?: Partial<Model<Api>["cost"]>;
188
+ contextWindow?: number;
189
+ maxTokens?: number;
190
+ omitMaxOutputTokens?: boolean;
191
+ headers?: Record<string, string>;
192
+ compat?: ModelSpec<Api>["compat"];
193
+ contextPromotionTarget?: string;
194
+ compactionModel?: string;
195
+ remoteCompaction?: RemoteCompactionConfig<Api>;
196
+ premiumMultiplier?: number;
197
+ }
198
+
199
+ /**
200
+ * How a patch treats the base model's transport metadata (headers/compat):
201
+ * - `merge`: fold the patch into the base's (modelOverrides semantics).
202
+ * - `replace`: the patch owns transport wholesale — same-id custom definitions
203
+ * already folded provider-level headers/compat in during parsing, so bundled
204
+ * transport metadata must not be re-merged (see `#mergeCustomModels`).
205
+ */
206
+ type ModelTransportPolicy = "merge" | "replace";
207
+ export function applyModelPatch(base: Model<Api>, patch: ModelPatch, transport: ModelTransportPolicy): Model<Api> {
208
+ const result = { ...base };
209
+ if (patch.name !== undefined) result.name = patch.name;
210
+ if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
211
+ if (patch.thinking !== undefined) result.thinking = patch.thinking;
212
+ if (patch.input !== undefined) result.input = patch.input;
213
+ if (patch.supportsTools !== undefined) result.supportsTools = patch.supportsTools;
214
+ if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
215
+ if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
216
+ if (patch.omitMaxOutputTokens !== undefined) result.omitMaxOutputTokens = patch.omitMaxOutputTokens;
217
+ if (patch.contextPromotionTarget !== undefined) result.contextPromotionTarget = patch.contextPromotionTarget;
218
+ if (patch.compactionModel !== undefined) result.compactionModel = patch.compactionModel;
219
+ if (patch.remoteCompaction !== undefined) {
220
+ result.remoteCompaction = mergeRemoteCompactionConfig(base.remoteCompaction, patch.remoteCompaction);
221
+ }
222
+ if (patch.premiumMultiplier !== undefined) result.premiumMultiplier = patch.premiumMultiplier;
223
+ if (patch.cost) {
224
+ result.cost = {
225
+ input: patch.cost.input ?? base.cost.input,
226
+ output: patch.cost.output ?? base.cost.output,
227
+ cacheRead: patch.cost.cacheRead ?? base.cost.cacheRead,
228
+ cacheWrite: patch.cost.cacheWrite ?? base.cost.cacheWrite,
229
+ };
230
+ }
231
+ let compat: ModelSpec<Api>["compat"];
232
+ if (transport === "merge") {
233
+ if (patch.headers) {
234
+ result.headers = { ...base.headers, ...patch.headers };
235
+ }
236
+ compat = mergeCompat(base.compatConfig, patch.compat);
237
+ } else {
238
+ result.headers = patch.headers;
239
+ compat = patch.compat;
240
+ }
241
+ const built = buildModel({ ...result, compat } as ModelSpec<Api>);
242
+ if (patch.thinking !== undefined && built.thinking !== undefined) {
243
+ // Config-authored capability metadata owns the explicit surface; build
244
+ // first so non-reasoning and wire-disabled models still suppress it.
245
+ built.thinking = patch.thinking;
246
+ }
247
+ return built;
248
+ }
249
+
250
+ export function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {
251
+ return applyModelPatch(model, override as ModelPatch, "merge");
252
+ }
@@ -0,0 +1,132 @@
1
+ import type { Api, Model } from "@oh-my-pi/pi-ai/types";
2
+ import { type OpenAICodexAccount, PROVIDER_DESCRIPTORS } from "@oh-my-pi/pi-catalog/provider-models";
3
+ import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
4
+
5
+ const SPECIAL_MODEL_MANAGER_PROVIDER_IDS: readonly string[] = [
6
+ "google-antigravity",
7
+ "google-gemini-cli",
8
+ "openai-codex",
9
+ ];
10
+
11
+ export const STARTUP_MODEL_CACHE_PROVIDER_IDS: readonly string[] = [
12
+ ...PROVIDER_DESCRIPTORS.map(descriptor => descriptor.providerId),
13
+ ...SPECIAL_MODEL_MANAGER_PROVIDER_IDS,
14
+ ];
15
+
16
+ // Sentinels for local-only OAuth tokens — declared inline to avoid loading
17
+ // provider modules at startup. Must match packages/ai/src/registry/llama-cpp.ts,
18
+ // packages/ai/src/registry/lm-studio.ts, and packages/ai/src/registry/vllm.ts.
19
+ const LOCAL_PROVIDER_PLACEHOLDERS = new Set<string>(["llama-cpp-local", "lm-studio-local", "vllm-local"]);
20
+
21
+ /**
22
+ * Hard bound for extension-provided fetchDynamicModels to prevent indefinite hangs
23
+ * during runtime provider discovery. Uses a cancellable manual timer (not AbortSignal.timeout)
24
+ * so a successful fast path does not leave an armed timeout signal for concurrent GC.
25
+ */
26
+ export const RUNTIME_DYNAMIC_MODEL_FETCH_TIMEOUT_MS = 15_000;
27
+ // Built-in discovery preflight mirror of the catalog model-manager's private
28
+ // cache timings (model-manager.ts: DEFAULT_CACHE_TTL_MS / NON_AUTHORITATIVE_RETRY_MS).
29
+ // Built-in descriptors never override cacheTtlMs, so agreeing with these values
30
+ // makes the OAuth-refresh preflight fire exactly when the manager will fetch.
31
+ export const BUILT_IN_DISCOVERY_CACHE_TTL_MS = 2 * 60 * 60 * 1000;
32
+ export const BUILT_IN_DISCOVERY_NON_AUTHORITATIVE_RETRY_MS = 5 * 60 * 1000;
33
+ export const kNoAuth = "N/A";
34
+
35
+ export function isAuthenticated(apiKey: string | undefined | null): apiKey is string {
36
+ return Boolean(apiKey) && apiKey !== kNoAuth;
37
+ }
38
+
39
+ export function isDiscoveryBearerApiKey(apiKey: string | undefined | null): apiKey is string {
40
+ return isAuthenticated(apiKey) && !LOCAL_PROVIDER_PLACEHOLDERS.has(apiKey);
41
+ }
42
+
43
+ /**
44
+ * Wraps an extension-provided fetchDynamicModels call with a hard timeout.
45
+ * Uses a cancellable manual timer (not AbortSignal.timeout) so that a fast
46
+ * successful path does not leave an armed timeout signal for concurrent GC.
47
+ * The inner fetcher does not receive a signal (extension contract has none).
48
+ */
49
+ export async function withRuntimeDynamicModelsTimeout<T>(timeoutMs: number, run: () => Promise<T>): Promise<T> {
50
+ const { promise: timeoutPromise, reject: timeoutReject } = Promise.withResolvers<never>();
51
+ const timer = setTimeout(() => {
52
+ timeoutReject(new Error(`fetchDynamicModels timed out after ${timeoutMs}ms`));
53
+ }, timeoutMs);
54
+ try {
55
+ return await Promise.race([run(), timeoutPromise]);
56
+ } finally {
57
+ clearTimeout(timer);
58
+ }
59
+ }
60
+
61
+ export interface BuiltInDiscoveryResult {
62
+ models: Model<Api>[];
63
+ authoritativeProviders: Set<string>;
64
+ }
65
+
66
+ export type ProviderDiscoveryStatus = "idle" | "ok" | "empty" | "cached" | "unavailable" | "unauthenticated";
67
+
68
+ export interface ProviderDiscoveryState {
69
+ provider: string;
70
+ status: ProviderDiscoveryStatus;
71
+ optional: boolean;
72
+ stale: boolean;
73
+ fetchedAt?: number;
74
+ models: string[];
75
+ error?: string;
76
+ }
77
+ export function extractGoogleOAuthToken(value: string | undefined): string | undefined {
78
+ if (!isAuthenticated(value)) return undefined;
79
+ try {
80
+ const parsed = JSON.parse(value) as { token?: unknown };
81
+ if (Object.hasOwn(parsed, "token")) {
82
+ if (typeof parsed.token !== "string") {
83
+ return undefined;
84
+ }
85
+ const token = parsed.token.trim();
86
+ return token.length > 0 ? token : undefined;
87
+ }
88
+ } catch {
89
+ // OAuth values for Google providers are expected to be JSON, but custom setups may already provide raw token.
90
+ }
91
+ return value;
92
+ }
93
+
94
+ export function getOAuthCredentialsForProvider(authStorage: AuthStorage, provider: string): OAuthCredential[] {
95
+ const providerEntry = authStorage.getAll()[provider];
96
+ if (!providerEntry) {
97
+ return [];
98
+ }
99
+ const entries = Array.isArray(providerEntry) ? providerEntry : [providerEntry];
100
+ return entries.filter((entry): entry is OAuthCredential => entry.type === "oauth");
101
+ }
102
+
103
+ /**
104
+ * Resolve every configured Codex OAuth account for catalog discovery, refreshing
105
+ * each credential exactly once. Codex `/models` is account-scoped, so discovery
106
+ * must fetch per account and union the results; resolving a single access token
107
+ * (as before) hid models available only through a sibling account (#6265).
108
+ *
109
+ * Returns `null` when any stored account fails to resolve (e.g. a transient
110
+ * refresh failure): the Codex manager is authoritative, so unioning only the
111
+ * accounts that resolved would cache a partial catalog and hide the failed
112
+ * account's models for the cache TTL. Aborting keeps the previous/bundled
113
+ * catalog instead.
114
+ */
115
+ export async function resolveCodexDiscoveryAccounts(
116
+ authStorage: AuthStorage,
117
+ resolvedAccessToken: string,
118
+ ): Promise<OpenAICodexAccount[] | null> {
119
+ const accesses = await authStorage.getOAuthAccesses("openai-codex");
120
+ const accounts: OpenAICodexAccount[] = [];
121
+ for (const access of accesses) {
122
+ if (!access.ok) return null;
123
+ accounts.push({ accessToken: access.accessToken, accountId: access.accountId });
124
+ }
125
+ if (!accounts.some(account => account.accessToken === resolvedAccessToken)) {
126
+ const matchingCredential = getOAuthCredentialsForProvider(authStorage, "openai-codex").find(
127
+ credential => credential.access === resolvedAccessToken,
128
+ );
129
+ accounts.push({ accessToken: resolvedAccessToken, accountId: matchingCredential?.accountId });
130
+ }
131
+ return accounts;
132
+ }