indusagi-coding-agent 0.1.0

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 (240) hide show
  1. package/CHANGELOG.md +2249 -0
  2. package/README.md +546 -0
  3. package/dist/cli/args.js +282 -0
  4. package/dist/cli/config-selector.js +30 -0
  5. package/dist/cli/file-processor.js +78 -0
  6. package/dist/cli/list-models.js +91 -0
  7. package/dist/cli/session-picker.js +31 -0
  8. package/dist/cli.js +10 -0
  9. package/dist/config.js +158 -0
  10. package/dist/core/agent-session.js +2097 -0
  11. package/dist/core/auth-storage.js +278 -0
  12. package/dist/core/bash-executor.js +211 -0
  13. package/dist/core/compaction/branch-summarization.js +241 -0
  14. package/dist/core/compaction/compaction.js +606 -0
  15. package/dist/core/compaction/index.js +6 -0
  16. package/dist/core/compaction/utils.js +137 -0
  17. package/dist/core/diagnostics.js +1 -0
  18. package/dist/core/event-bus.js +24 -0
  19. package/dist/core/exec.js +70 -0
  20. package/dist/core/export-html/ansi-to-html.js +248 -0
  21. package/dist/core/export-html/index.js +221 -0
  22. package/dist/core/export-html/template.css +905 -0
  23. package/dist/core/export-html/template.html +54 -0
  24. package/dist/core/export-html/template.js +1549 -0
  25. package/dist/core/export-html/tool-renderer.js +56 -0
  26. package/dist/core/export-html/vendor/highlight.min.js +1213 -0
  27. package/dist/core/export-html/vendor/marked.min.js +6 -0
  28. package/dist/core/extensions/index.js +8 -0
  29. package/dist/core/extensions/loader.js +395 -0
  30. package/dist/core/extensions/runner.js +499 -0
  31. package/dist/core/extensions/types.js +31 -0
  32. package/dist/core/extensions/wrapper.js +101 -0
  33. package/dist/core/footer-data-provider.js +133 -0
  34. package/dist/core/index.js +8 -0
  35. package/dist/core/keybindings.js +140 -0
  36. package/dist/core/messages.js +122 -0
  37. package/dist/core/model-registry.js +454 -0
  38. package/dist/core/model-resolver.js +309 -0
  39. package/dist/core/package-manager.js +1142 -0
  40. package/dist/core/prompt-templates.js +250 -0
  41. package/dist/core/resource-loader.js +569 -0
  42. package/dist/core/sdk.js +225 -0
  43. package/dist/core/session-manager.js +1078 -0
  44. package/dist/core/settings-manager.js +430 -0
  45. package/dist/core/skills.js +339 -0
  46. package/dist/core/system-prompt.js +136 -0
  47. package/dist/core/timings.js +24 -0
  48. package/dist/core/tools/bash.js +226 -0
  49. package/dist/core/tools/edit-diff.js +242 -0
  50. package/dist/core/tools/edit.js +145 -0
  51. package/dist/core/tools/find.js +205 -0
  52. package/dist/core/tools/grep.js +238 -0
  53. package/dist/core/tools/index.js +60 -0
  54. package/dist/core/tools/ls.js +117 -0
  55. package/dist/core/tools/path-utils.js +52 -0
  56. package/dist/core/tools/read.js +165 -0
  57. package/dist/core/tools/truncate.js +204 -0
  58. package/dist/core/tools/write.js +77 -0
  59. package/dist/index.js +41 -0
  60. package/dist/main.js +565 -0
  61. package/dist/migrations.js +260 -0
  62. package/dist/modes/index.js +7 -0
  63. package/dist/modes/interactive/components/armin.js +328 -0
  64. package/dist/modes/interactive/components/assistant-message.js +86 -0
  65. package/dist/modes/interactive/components/bash-execution.js +155 -0
  66. package/dist/modes/interactive/components/bordered-loader.js +47 -0
  67. package/dist/modes/interactive/components/branch-summary-message.js +41 -0
  68. package/dist/modes/interactive/components/compaction-summary-message.js +42 -0
  69. package/dist/modes/interactive/components/config-selector.js +458 -0
  70. package/dist/modes/interactive/components/countdown-timer.js +27 -0
  71. package/dist/modes/interactive/components/custom-editor.js +61 -0
  72. package/dist/modes/interactive/components/custom-message.js +80 -0
  73. package/dist/modes/interactive/components/diff.js +132 -0
  74. package/dist/modes/interactive/components/dynamic-border.js +19 -0
  75. package/dist/modes/interactive/components/extension-editor.js +96 -0
  76. package/dist/modes/interactive/components/extension-input.js +54 -0
  77. package/dist/modes/interactive/components/extension-selector.js +70 -0
  78. package/dist/modes/interactive/components/footer.js +213 -0
  79. package/dist/modes/interactive/components/index.js +31 -0
  80. package/dist/modes/interactive/components/keybinding-hints.js +60 -0
  81. package/dist/modes/interactive/components/login-dialog.js +138 -0
  82. package/dist/modes/interactive/components/model-selector.js +253 -0
  83. package/dist/modes/interactive/components/oauth-selector.js +91 -0
  84. package/dist/modes/interactive/components/scoped-models-selector.js +262 -0
  85. package/dist/modes/interactive/components/session-selector-search.js +145 -0
  86. package/dist/modes/interactive/components/session-selector.js +698 -0
  87. package/dist/modes/interactive/components/settings-selector.js +250 -0
  88. package/dist/modes/interactive/components/show-images-selector.js +33 -0
  89. package/dist/modes/interactive/components/skill-invocation-message.js +44 -0
  90. package/dist/modes/interactive/components/theme-selector.js +43 -0
  91. package/dist/modes/interactive/components/thinking-selector.js +45 -0
  92. package/dist/modes/interactive/components/tool-execution.js +608 -0
  93. package/dist/modes/interactive/components/tree-selector.js +892 -0
  94. package/dist/modes/interactive/components/user-message-selector.js +109 -0
  95. package/dist/modes/interactive/components/user-message.js +15 -0
  96. package/dist/modes/interactive/components/visual-truncate.js +32 -0
  97. package/dist/modes/interactive/interactive-mode.js +3576 -0
  98. package/dist/modes/interactive/theme/dark.json +85 -0
  99. package/dist/modes/interactive/theme/light.json +84 -0
  100. package/dist/modes/interactive/theme/theme-schema.json +335 -0
  101. package/dist/modes/interactive/theme/theme.js +938 -0
  102. package/dist/modes/print-mode.js +96 -0
  103. package/dist/modes/rpc/rpc-client.js +390 -0
  104. package/dist/modes/rpc/rpc-mode.js +448 -0
  105. package/dist/modes/rpc/rpc-types.js +7 -0
  106. package/dist/utils/changelog.js +86 -0
  107. package/dist/utils/clipboard-image.js +116 -0
  108. package/dist/utils/clipboard.js +58 -0
  109. package/dist/utils/frontmatter.js +25 -0
  110. package/dist/utils/git.js +5 -0
  111. package/dist/utils/image-convert.js +34 -0
  112. package/dist/utils/image-resize.js +180 -0
  113. package/dist/utils/mime.js +25 -0
  114. package/dist/utils/photon.js +120 -0
  115. package/dist/utils/shell.js +164 -0
  116. package/dist/utils/sleep.js +16 -0
  117. package/dist/utils/tools-manager.js +186 -0
  118. package/docs/compaction.md +390 -0
  119. package/docs/custom-provider.md +538 -0
  120. package/docs/development.md +69 -0
  121. package/docs/extensions.md +1733 -0
  122. package/docs/images/doom-extension.png +0 -0
  123. package/docs/images/interactive-mode.png +0 -0
  124. package/docs/images/tree-view.png +0 -0
  125. package/docs/json.md +79 -0
  126. package/docs/keybindings.md +162 -0
  127. package/docs/models.md +193 -0
  128. package/docs/packages.md +163 -0
  129. package/docs/prompt-templates.md +67 -0
  130. package/docs/providers.md +147 -0
  131. package/docs/rpc.md +1048 -0
  132. package/docs/sdk.md +957 -0
  133. package/docs/session.md +412 -0
  134. package/docs/settings.md +216 -0
  135. package/docs/shell-aliases.md +13 -0
  136. package/docs/skills.md +226 -0
  137. package/docs/terminal-setup.md +65 -0
  138. package/docs/themes.md +295 -0
  139. package/docs/tree.md +219 -0
  140. package/docs/tui.md +887 -0
  141. package/docs/windows.md +17 -0
  142. package/examples/README.md +25 -0
  143. package/examples/extensions/README.md +192 -0
  144. package/examples/extensions/antigravity-image-gen.ts +414 -0
  145. package/examples/extensions/auto-commit-on-exit.ts +49 -0
  146. package/examples/extensions/bookmark.ts +50 -0
  147. package/examples/extensions/claude-rules.ts +86 -0
  148. package/examples/extensions/confirm-destructive.ts +59 -0
  149. package/examples/extensions/custom-compaction.ts +115 -0
  150. package/examples/extensions/custom-footer.ts +65 -0
  151. package/examples/extensions/custom-header.ts +73 -0
  152. package/examples/extensions/custom-provider-anthropic/index.ts +605 -0
  153. package/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
  154. package/examples/extensions/custom-provider-anthropic/package.json +19 -0
  155. package/examples/extensions/custom-provider-gitlab-duo/index.ts +350 -0
  156. package/examples/extensions/custom-provider-gitlab-duo/package.json +16 -0
  157. package/examples/extensions/custom-provider-gitlab-duo/test.ts +83 -0
  158. package/examples/extensions/dirty-repo-guard.ts +56 -0
  159. package/examples/extensions/doom-overlay/README.md +46 -0
  160. package/examples/extensions/doom-overlay/doom/build/doom.js +21 -0
  161. package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
  162. package/examples/extensions/doom-overlay/doom/build.sh +152 -0
  163. package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -0
  164. package/examples/extensions/doom-overlay/doom-component.ts +133 -0
  165. package/examples/extensions/doom-overlay/doom-engine.ts +173 -0
  166. package/examples/extensions/doom-overlay/doom-keys.ts +105 -0
  167. package/examples/extensions/doom-overlay/index.ts +74 -0
  168. package/examples/extensions/doom-overlay/wad-finder.ts +51 -0
  169. package/examples/extensions/event-bus.ts +43 -0
  170. package/examples/extensions/file-trigger.ts +41 -0
  171. package/examples/extensions/git-checkpoint.ts +53 -0
  172. package/examples/extensions/handoff.ts +151 -0
  173. package/examples/extensions/hello.ts +25 -0
  174. package/examples/extensions/inline-bash.ts +94 -0
  175. package/examples/extensions/input-transform.ts +43 -0
  176. package/examples/extensions/interactive-shell.ts +196 -0
  177. package/examples/extensions/mac-system-theme.ts +47 -0
  178. package/examples/extensions/message-renderer.ts +60 -0
  179. package/examples/extensions/modal-editor.ts +86 -0
  180. package/examples/extensions/model-status.ts +31 -0
  181. package/examples/extensions/notify.ts +25 -0
  182. package/examples/extensions/overlay-qa-tests.ts +882 -0
  183. package/examples/extensions/overlay-test.ts +151 -0
  184. package/examples/extensions/permission-gate.ts +34 -0
  185. package/examples/extensions/pirate.ts +47 -0
  186. package/examples/extensions/plan-mode/README.md +65 -0
  187. package/examples/extensions/plan-mode/index.ts +341 -0
  188. package/examples/extensions/plan-mode/utils.ts +168 -0
  189. package/examples/extensions/preset.ts +399 -0
  190. package/examples/extensions/protected-paths.ts +30 -0
  191. package/examples/extensions/qna.ts +120 -0
  192. package/examples/extensions/question.ts +265 -0
  193. package/examples/extensions/questionnaire.ts +428 -0
  194. package/examples/extensions/rainbow-editor.ts +88 -0
  195. package/examples/extensions/sandbox/index.ts +318 -0
  196. package/examples/extensions/sandbox/package-lock.json +92 -0
  197. package/examples/extensions/sandbox/package.json +19 -0
  198. package/examples/extensions/send-user-message.ts +97 -0
  199. package/examples/extensions/session-name.ts +27 -0
  200. package/examples/extensions/shutdown-command.ts +63 -0
  201. package/examples/extensions/snake.ts +344 -0
  202. package/examples/extensions/space-invaders.ts +561 -0
  203. package/examples/extensions/ssh.ts +220 -0
  204. package/examples/extensions/status-line.ts +40 -0
  205. package/examples/extensions/subagent/README.md +172 -0
  206. package/examples/extensions/subagent/agents/planner.md +37 -0
  207. package/examples/extensions/subagent/agents/reviewer.md +35 -0
  208. package/examples/extensions/subagent/agents/scout.md +50 -0
  209. package/examples/extensions/subagent/agents/worker.md +24 -0
  210. package/examples/extensions/subagent/agents.ts +127 -0
  211. package/examples/extensions/subagent/index.ts +964 -0
  212. package/examples/extensions/subagent/prompts/implement-and-review.md +10 -0
  213. package/examples/extensions/subagent/prompts/implement.md +10 -0
  214. package/examples/extensions/subagent/prompts/scout-and-plan.md +9 -0
  215. package/examples/extensions/summarize.ts +196 -0
  216. package/examples/extensions/timed-confirm.ts +70 -0
  217. package/examples/extensions/todo.ts +300 -0
  218. package/examples/extensions/tool-override.ts +144 -0
  219. package/examples/extensions/tools.ts +147 -0
  220. package/examples/extensions/trigger-compact.ts +40 -0
  221. package/examples/extensions/truncated-tool.ts +193 -0
  222. package/examples/extensions/widget-placement.ts +17 -0
  223. package/examples/extensions/with-deps/index.ts +36 -0
  224. package/examples/extensions/with-deps/package-lock.json +31 -0
  225. package/examples/extensions/with-deps/package.json +22 -0
  226. package/examples/sdk/01-minimal.ts +22 -0
  227. package/examples/sdk/02-custom-model.ts +50 -0
  228. package/examples/sdk/03-custom-prompt.ts +55 -0
  229. package/examples/sdk/04-skills.ts +46 -0
  230. package/examples/sdk/05-tools.ts +56 -0
  231. package/examples/sdk/06-extensions.ts +88 -0
  232. package/examples/sdk/07-context-files.ts +40 -0
  233. package/examples/sdk/08-prompt-templates.ts +47 -0
  234. package/examples/sdk/09-api-keys-and-oauth.ts +48 -0
  235. package/examples/sdk/10-settings.ts +38 -0
  236. package/examples/sdk/11-sessions.ts +48 -0
  237. package/examples/sdk/12-full-control.ts +82 -0
  238. package/examples/sdk/13-codex-oauth.ts +37 -0
  239. package/examples/sdk/README.md +144 -0
  240. package/package.json +85 -0
@@ -0,0 +1,454 @@
1
+ /**
2
+ * Model registry - manages built-in and custom models, provides API key resolution.
3
+ */
4
+ import { getModels, getProviders, registerApiProvider, registerOAuthProvider, } from "indusagi/ai";
5
+ import { Type } from "@sinclair/typebox";
6
+ import AjvModule from "ajv";
7
+ import { execSync } from "child_process";
8
+ import { existsSync, readFileSync } from "fs";
9
+ import { join } from "path";
10
+ import { getAgentDir } from "../config.js";
11
+ const Ajv = AjvModule.default || AjvModule;
12
+ // Schema for OpenRouter routing preferences
13
+ const OpenRouterRoutingSchema = Type.Object({
14
+ only: Type.Optional(Type.Array(Type.String())),
15
+ order: Type.Optional(Type.Array(Type.String())),
16
+ });
17
+ // Schema for OpenAI compatibility settings
18
+ const OpenAICompletionsCompatSchema = Type.Object({
19
+ supportsStore: Type.Optional(Type.Boolean()),
20
+ supportsDeveloperRole: Type.Optional(Type.Boolean()),
21
+ supportsReasoningEffort: Type.Optional(Type.Boolean()),
22
+ supportsUsageInStreaming: Type.Optional(Type.Boolean()),
23
+ maxTokensField: Type.Optional(Type.Union([Type.Literal("max_completion_tokens"), Type.Literal("max_tokens")])),
24
+ requiresToolResultName: Type.Optional(Type.Boolean()),
25
+ requiresAssistantAfterToolResult: Type.Optional(Type.Boolean()),
26
+ requiresThinkingAsText: Type.Optional(Type.Boolean()),
27
+ requiresMistralToolIds: Type.Optional(Type.Boolean()),
28
+ thinkingFormat: Type.Optional(Type.Union([Type.Literal("openai"), Type.Literal("zai")])),
29
+ openRouterRouting: Type.Optional(OpenRouterRoutingSchema),
30
+ });
31
+ const OpenAIResponsesCompatSchema = Type.Object({
32
+ // Reserved for future use
33
+ });
34
+ const OpenAICompatSchema = Type.Union([OpenAICompletionsCompatSchema, OpenAIResponsesCompatSchema]);
35
+ // Schema for custom model definition
36
+ const ModelDefinitionSchema = Type.Object({
37
+ id: Type.String({ minLength: 1 }),
38
+ name: Type.String({ minLength: 1 }),
39
+ api: Type.Optional(Type.String({ minLength: 1 })),
40
+ reasoning: Type.Boolean(),
41
+ input: Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")])),
42
+ cost: Type.Object({
43
+ input: Type.Number(),
44
+ output: Type.Number(),
45
+ cacheRead: Type.Number(),
46
+ cacheWrite: Type.Number(),
47
+ }),
48
+ contextWindow: Type.Number(),
49
+ maxTokens: Type.Number(),
50
+ headers: Type.Optional(Type.Record(Type.String(), Type.String())),
51
+ compat: Type.Optional(OpenAICompatSchema),
52
+ });
53
+ const ProviderConfigSchema = Type.Object({
54
+ baseUrl: Type.Optional(Type.String({ minLength: 1 })),
55
+ apiKey: Type.Optional(Type.String({ minLength: 1 })),
56
+ api: Type.Optional(Type.String({ minLength: 1 })),
57
+ headers: Type.Optional(Type.Record(Type.String(), Type.String())),
58
+ authHeader: Type.Optional(Type.Boolean()),
59
+ models: Type.Optional(Type.Array(ModelDefinitionSchema)),
60
+ });
61
+ const ModelsConfigSchema = Type.Object({
62
+ providers: Type.Record(Type.String(), ProviderConfigSchema),
63
+ });
64
+ function emptyCustomModelsResult(error) {
65
+ return { models: [], replacedProviders: new Set(), overrides: new Map(), error };
66
+ }
67
+ // Cache for shell command results (persists for process lifetime)
68
+ const commandResultCache = new Map();
69
+ /**
70
+ * Resolve a config value (API key, header value, etc.) to an actual value.
71
+ * - If starts with "!", executes the rest as a shell command and uses stdout (cached)
72
+ * - Otherwise checks environment variable first, then treats as literal (not cached)
73
+ */
74
+ function resolveConfigValue(config) {
75
+ if (config.startsWith("!")) {
76
+ return executeCommand(config);
77
+ }
78
+ const envValue = process.env[config];
79
+ return envValue || config;
80
+ }
81
+ function executeCommand(commandConfig) {
82
+ if (commandResultCache.has(commandConfig)) {
83
+ return commandResultCache.get(commandConfig);
84
+ }
85
+ const command = commandConfig.slice(1);
86
+ let result;
87
+ try {
88
+ const output = execSync(command, {
89
+ encoding: "utf-8",
90
+ timeout: 10000,
91
+ stdio: ["ignore", "pipe", "ignore"],
92
+ });
93
+ result = output.trim() || undefined;
94
+ }
95
+ catch {
96
+ result = undefined;
97
+ }
98
+ commandResultCache.set(commandConfig, result);
99
+ return result;
100
+ }
101
+ /**
102
+ * Resolve all header values using the same resolution logic as API keys.
103
+ */
104
+ function resolveHeaders(headers) {
105
+ if (!headers)
106
+ return undefined;
107
+ const resolved = {};
108
+ for (const [key, value] of Object.entries(headers)) {
109
+ const resolvedValue = resolveConfigValue(value);
110
+ if (resolvedValue) {
111
+ resolved[key] = resolvedValue;
112
+ }
113
+ }
114
+ return Object.keys(resolved).length > 0 ? resolved : undefined;
115
+ }
116
+ /** Clear the config value command cache. Exported for testing. */
117
+ export function clearApiKeyCache() {
118
+ commandResultCache.clear();
119
+ }
120
+ /**
121
+ * Model registry - loads and manages models, resolves API keys via AuthStorage.
122
+ */
123
+ export class ModelRegistry {
124
+ constructor(authStorage, modelsJsonPath = join(getAgentDir(), "models.json")) {
125
+ this.authStorage = authStorage;
126
+ this.modelsJsonPath = modelsJsonPath;
127
+ this.models = [];
128
+ this.customProviderApiKeys = new Map();
129
+ this.registeredProviders = new Map();
130
+ this.loadError = undefined;
131
+ // Set up fallback resolver for custom provider API keys
132
+ this.authStorage.setFallbackResolver((provider) => {
133
+ const keyConfig = this.customProviderApiKeys.get(provider);
134
+ if (keyConfig) {
135
+ return resolveConfigValue(keyConfig);
136
+ }
137
+ return undefined;
138
+ });
139
+ // Load models
140
+ this.loadModels();
141
+ }
142
+ /**
143
+ * Reload models from disk (built-in + custom from models.json).
144
+ */
145
+ refresh() {
146
+ this.customProviderApiKeys.clear();
147
+ this.loadError = undefined;
148
+ this.loadModels();
149
+ for (const [providerName, config] of this.registeredProviders.entries()) {
150
+ this.applyProviderConfig(providerName, config);
151
+ }
152
+ }
153
+ /**
154
+ * Get any error from loading models.json (undefined if no error).
155
+ */
156
+ getError() {
157
+ return this.loadError;
158
+ }
159
+ loadModels() {
160
+ // Load custom models from models.json first (to know which providers to skip/override)
161
+ const { models: customModels, replacedProviders, overrides, error, } = this.modelsJsonPath ? this.loadCustomModels(this.modelsJsonPath) : emptyCustomModelsResult();
162
+ if (error) {
163
+ this.loadError = error;
164
+ // Keep built-in models even if custom models failed to load
165
+ }
166
+ const builtInModels = this.loadBuiltInModels(replacedProviders, overrides);
167
+ let combined = [...builtInModels, ...customModels];
168
+ // Let OAuth providers modify their models (e.g., update baseUrl)
169
+ for (const oauthProvider of this.authStorage.getOAuthProviders()) {
170
+ const cred = this.authStorage.get(oauthProvider.id);
171
+ if (cred?.type === "oauth" && oauthProvider.modifyModels) {
172
+ combined = oauthProvider.modifyModels(combined, cred);
173
+ }
174
+ }
175
+ this.models = combined;
176
+ }
177
+ /** Load built-in models, skipping replaced providers and applying overrides */
178
+ loadBuiltInModels(replacedProviders, overrides) {
179
+ return getProviders()
180
+ .filter((provider) => !replacedProviders.has(provider))
181
+ .flatMap((provider) => {
182
+ const models = getModels(provider);
183
+ const override = overrides.get(provider);
184
+ if (!override)
185
+ return models;
186
+ // Apply baseUrl/headers override to all models of this provider
187
+ const resolvedHeaders = resolveHeaders(override.headers);
188
+ return models.map((m) => ({
189
+ ...m,
190
+ baseUrl: override.baseUrl ?? m.baseUrl,
191
+ headers: resolvedHeaders ? { ...m.headers, ...resolvedHeaders } : m.headers,
192
+ }));
193
+ });
194
+ }
195
+ loadCustomModels(modelsJsonPath) {
196
+ if (!existsSync(modelsJsonPath)) {
197
+ return emptyCustomModelsResult();
198
+ }
199
+ try {
200
+ const content = readFileSync(modelsJsonPath, "utf-8");
201
+ const config = JSON.parse(content);
202
+ // Validate schema
203
+ const ajv = new Ajv();
204
+ const validate = ajv.compile(ModelsConfigSchema);
205
+ if (!validate(config)) {
206
+ const errors = validate.errors?.map((e) => ` - ${e.instancePath || "root"}: ${e.message}`).join("\n") ||
207
+ "Unknown schema error";
208
+ return emptyCustomModelsResult(`Invalid models.json schema:\n${errors}\n\nFile: ${modelsJsonPath}`);
209
+ }
210
+ // Additional validation
211
+ this.validateConfig(config);
212
+ // Separate providers into "full replacement" (has models) vs "override-only" (no models)
213
+ const replacedProviders = new Set();
214
+ const overrides = new Map();
215
+ for (const [providerName, providerConfig] of Object.entries(config.providers)) {
216
+ if (providerConfig.models && providerConfig.models.length > 0) {
217
+ // Has custom models -> full replacement
218
+ replacedProviders.add(providerName);
219
+ }
220
+ else {
221
+ // No models -> just override baseUrl/headers on built-in
222
+ overrides.set(providerName, {
223
+ baseUrl: providerConfig.baseUrl,
224
+ headers: providerConfig.headers,
225
+ apiKey: providerConfig.apiKey,
226
+ });
227
+ // Store API key for fallback resolver
228
+ if (providerConfig.apiKey) {
229
+ this.customProviderApiKeys.set(providerName, providerConfig.apiKey);
230
+ }
231
+ }
232
+ }
233
+ return { models: this.parseModels(config), replacedProviders, overrides, error: undefined };
234
+ }
235
+ catch (error) {
236
+ if (error instanceof SyntaxError) {
237
+ return emptyCustomModelsResult(`Failed to parse models.json: ${error.message}\n\nFile: ${modelsJsonPath}`);
238
+ }
239
+ return emptyCustomModelsResult(`Failed to load models.json: ${error instanceof Error ? error.message : error}\n\nFile: ${modelsJsonPath}`);
240
+ }
241
+ }
242
+ validateConfig(config) {
243
+ for (const [providerName, providerConfig] of Object.entries(config.providers)) {
244
+ const hasProviderApi = !!providerConfig.api;
245
+ const models = providerConfig.models ?? [];
246
+ if (models.length === 0) {
247
+ // Override-only config: just needs baseUrl (to override built-in)
248
+ if (!providerConfig.baseUrl) {
249
+ throw new Error(`Provider ${providerName}: must specify either "baseUrl" (for override) or "models" (for replacement).`);
250
+ }
251
+ }
252
+ else {
253
+ // Full replacement: needs baseUrl and apiKey
254
+ if (!providerConfig.baseUrl) {
255
+ throw new Error(`Provider ${providerName}: "baseUrl" is required when defining custom models.`);
256
+ }
257
+ if (!providerConfig.apiKey) {
258
+ throw new Error(`Provider ${providerName}: "apiKey" is required when defining custom models.`);
259
+ }
260
+ }
261
+ for (const modelDef of models) {
262
+ const hasModelApi = !!modelDef.api;
263
+ if (!hasProviderApi && !hasModelApi) {
264
+ throw new Error(`Provider ${providerName}, model ${modelDef.id}: no "api" specified. Set at provider or model level.`);
265
+ }
266
+ if (!modelDef.id)
267
+ throw new Error(`Provider ${providerName}: model missing "id"`);
268
+ if (!modelDef.name)
269
+ throw new Error(`Provider ${providerName}: model missing "name"`);
270
+ if (modelDef.contextWindow <= 0)
271
+ throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);
272
+ if (modelDef.maxTokens <= 0)
273
+ throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);
274
+ }
275
+ }
276
+ }
277
+ parseModels(config) {
278
+ const models = [];
279
+ for (const [providerName, providerConfig] of Object.entries(config.providers)) {
280
+ const modelDefs = providerConfig.models ?? [];
281
+ if (modelDefs.length === 0)
282
+ continue; // Override-only, no custom models
283
+ // Store API key config for fallback resolver
284
+ if (providerConfig.apiKey) {
285
+ this.customProviderApiKeys.set(providerName, providerConfig.apiKey);
286
+ }
287
+ for (const modelDef of modelDefs) {
288
+ const api = modelDef.api || providerConfig.api;
289
+ if (!api)
290
+ continue;
291
+ // Merge headers: provider headers are base, model headers override
292
+ // Resolve env vars and shell commands in header values
293
+ const providerHeaders = resolveHeaders(providerConfig.headers);
294
+ const modelHeaders = resolveHeaders(modelDef.headers);
295
+ let headers = providerHeaders || modelHeaders ? { ...providerHeaders, ...modelHeaders } : undefined;
296
+ // If authHeader is true, add Authorization header with resolved API key
297
+ if (providerConfig.authHeader && providerConfig.apiKey) {
298
+ const resolvedKey = resolveConfigValue(providerConfig.apiKey);
299
+ if (resolvedKey) {
300
+ headers = { ...headers, Authorization: `Bearer ${resolvedKey}` };
301
+ }
302
+ }
303
+ // baseUrl is validated to exist for providers with models
304
+ models.push({
305
+ id: modelDef.id,
306
+ name: modelDef.name,
307
+ api: api,
308
+ provider: providerName,
309
+ baseUrl: providerConfig.baseUrl,
310
+ reasoning: modelDef.reasoning,
311
+ input: modelDef.input,
312
+ cost: modelDef.cost,
313
+ contextWindow: modelDef.contextWindow,
314
+ maxTokens: modelDef.maxTokens,
315
+ headers,
316
+ compat: modelDef.compat,
317
+ });
318
+ }
319
+ }
320
+ return models;
321
+ }
322
+ /**
323
+ * Get all models (built-in + custom).
324
+ * If models.json had errors, returns only built-in models.
325
+ */
326
+ getAll() {
327
+ return this.models;
328
+ }
329
+ /**
330
+ * Get only models that have auth configured.
331
+ * This is a fast check that doesn't refresh OAuth tokens.
332
+ */
333
+ getAvailable() {
334
+ return this.models.filter((m) => this.authStorage.hasAuth(m.provider));
335
+ }
336
+ /**
337
+ * Find a model by provider and ID.
338
+ */
339
+ find(provider, modelId) {
340
+ return this.models.find((m) => m.provider === provider && m.id === modelId);
341
+ }
342
+ /**
343
+ * Get API key for a model.
344
+ */
345
+ async getApiKey(model) {
346
+ return this.authStorage.getApiKey(model.provider);
347
+ }
348
+ /**
349
+ * Get API key for a provider.
350
+ */
351
+ async getApiKeyForProvider(provider) {
352
+ return this.authStorage.getApiKey(provider);
353
+ }
354
+ /**
355
+ * Check if a model is using OAuth credentials (subscription).
356
+ */
357
+ isUsingOAuth(model) {
358
+ const cred = this.authStorage.get(model.provider);
359
+ return cred?.type === "oauth";
360
+ }
361
+ /**
362
+ * Register a provider dynamically (from extensions).
363
+ *
364
+ * If provider has models: replaces all existing models for this provider.
365
+ * If provider has only baseUrl/headers: overrides existing models' URLs.
366
+ * If provider has oauth: registers OAuth provider for /login support.
367
+ */
368
+ registerProvider(providerName, config) {
369
+ this.registeredProviders.set(providerName, config);
370
+ this.applyProviderConfig(providerName, config);
371
+ }
372
+ applyProviderConfig(providerName, config) {
373
+ // Register OAuth provider if provided
374
+ if (config.oauth) {
375
+ // Ensure the OAuth provider ID matches the provider name
376
+ const oauthProvider = {
377
+ ...config.oauth,
378
+ id: providerName,
379
+ };
380
+ registerOAuthProvider(oauthProvider);
381
+ }
382
+ if (config.streamSimple) {
383
+ if (!config.api) {
384
+ throw new Error(`Provider ${providerName}: "api" is required when registering streamSimple.`);
385
+ }
386
+ const streamSimple = config.streamSimple;
387
+ registerApiProvider({
388
+ api: config.api,
389
+ stream: (model, context, options) => streamSimple(model, context, options),
390
+ streamSimple,
391
+ });
392
+ }
393
+ // Store API key for auth resolution
394
+ if (config.apiKey) {
395
+ this.customProviderApiKeys.set(providerName, config.apiKey);
396
+ }
397
+ if (config.models && config.models.length > 0) {
398
+ // Full replacement: remove existing models for this provider
399
+ this.models = this.models.filter((m) => m.provider !== providerName);
400
+ // Validate required fields
401
+ if (!config.baseUrl) {
402
+ throw new Error(`Provider ${providerName}: "baseUrl" is required when defining models.`);
403
+ }
404
+ if (!config.apiKey && !config.oauth) {
405
+ throw new Error(`Provider ${providerName}: "apiKey" or "oauth" is required when defining models.`);
406
+ }
407
+ // Parse and add new models
408
+ for (const modelDef of config.models) {
409
+ const api = modelDef.api || config.api;
410
+ if (!api) {
411
+ throw new Error(`Provider ${providerName}, model ${modelDef.id}: no "api" specified.`);
412
+ }
413
+ // Merge headers
414
+ const providerHeaders = resolveHeaders(config.headers);
415
+ const modelHeaders = resolveHeaders(modelDef.headers);
416
+ let headers = providerHeaders || modelHeaders ? { ...providerHeaders, ...modelHeaders } : undefined;
417
+ // If authHeader is true, add Authorization header
418
+ if (config.authHeader && config.apiKey) {
419
+ const resolvedKey = resolveConfigValue(config.apiKey);
420
+ if (resolvedKey) {
421
+ headers = { ...headers, Authorization: `Bearer ${resolvedKey}` };
422
+ }
423
+ }
424
+ this.models.push({
425
+ id: modelDef.id,
426
+ name: modelDef.name,
427
+ api: api,
428
+ provider: providerName,
429
+ baseUrl: config.baseUrl,
430
+ reasoning: modelDef.reasoning,
431
+ input: modelDef.input,
432
+ cost: modelDef.cost,
433
+ contextWindow: modelDef.contextWindow,
434
+ maxTokens: modelDef.maxTokens,
435
+ headers,
436
+ compat: modelDef.compat,
437
+ });
438
+ }
439
+ }
440
+ else if (config.baseUrl) {
441
+ // Override-only: update baseUrl/headers for existing models
442
+ const resolvedHeaders = resolveHeaders(config.headers);
443
+ this.models = this.models.map((m) => {
444
+ if (m.provider !== providerName)
445
+ return m;
446
+ return {
447
+ ...m,
448
+ baseUrl: config.baseUrl ?? m.baseUrl,
449
+ headers: resolvedHeaders ? { ...m.headers, ...resolvedHeaders } : m.headers,
450
+ };
451
+ });
452
+ }
453
+ }
454
+ }