@leanandmean/coding-agent 0.74.1-scramjet.7 → 0.74.1-scramjet.71

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 (226) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +202 -0
  3. package/README.md +37 -9
  4. package/dist/cli/args.d.ts +2 -0
  5. package/dist/cli/args.d.ts.map +1 -1
  6. package/dist/cli/args.js +45 -1
  7. package/dist/cli/args.js.map +1 -1
  8. package/dist/cli/initial-message.d.ts +1 -0
  9. package/dist/cli/initial-message.d.ts.map +1 -1
  10. package/dist/cli/initial-message.js +3 -0
  11. package/dist/cli/initial-message.js.map +1 -1
  12. package/dist/cli/list-models.d.ts +2 -0
  13. package/dist/cli/list-models.d.ts.map +1 -1
  14. package/dist/cli/list-models.js +4 -1
  15. package/dist/cli/list-models.js.map +1 -1
  16. package/dist/config.d.ts +9 -2
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +33 -22
  19. package/dist/config.js.map +1 -1
  20. package/dist/core/agent-session-runtime.d.ts +12 -3
  21. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  22. package/dist/core/agent-session-runtime.js +64 -38
  23. package/dist/core/agent-session-runtime.js.map +1 -1
  24. package/dist/core/agent-session-services.d.ts +2 -0
  25. package/dist/core/agent-session-services.d.ts.map +1 -1
  26. package/dist/core/agent-session-services.js +3 -0
  27. package/dist/core/agent-session-services.js.map +1 -1
  28. package/dist/core/agent-session.d.ts +84 -17
  29. package/dist/core/agent-session.d.ts.map +1 -1
  30. package/dist/core/agent-session.js +1082 -209
  31. package/dist/core/agent-session.js.map +1 -1
  32. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  33. package/dist/core/compaction/branch-summarization.js +21 -17
  34. package/dist/core/compaction/branch-summarization.js.map +1 -1
  35. package/dist/core/compaction/compaction.d.ts +3 -5
  36. package/dist/core/compaction/compaction.d.ts.map +1 -1
  37. package/dist/core/compaction/compaction.js +43 -10
  38. package/dist/core/compaction/compaction.js.map +1 -1
  39. package/dist/core/exec.d.ts +12 -0
  40. package/dist/core/exec.d.ts.map +1 -1
  41. package/dist/core/exec.js +74 -28
  42. package/dist/core/exec.js.map +1 -1
  43. package/dist/core/export-html/template.js +4 -0
  44. package/dist/core/extensions/index.d.ts +1 -1
  45. package/dist/core/extensions/index.d.ts.map +1 -1
  46. package/dist/core/extensions/index.js.map +1 -1
  47. package/dist/core/extensions/runner.d.ts +9 -5
  48. package/dist/core/extensions/runner.d.ts.map +1 -1
  49. package/dist/core/extensions/runner.js +160 -3
  50. package/dist/core/extensions/runner.js.map +1 -1
  51. package/dist/core/extensions/types.d.ts +85 -8
  52. package/dist/core/extensions/types.d.ts.map +1 -1
  53. package/dist/core/extensions/types.js.map +1 -1
  54. package/dist/core/keybindings.d.ts +8 -0
  55. package/dist/core/keybindings.d.ts.map +1 -1
  56. package/dist/core/model-registry.d.ts +103 -0
  57. package/dist/core/model-registry.d.ts.map +1 -1
  58. package/dist/core/model-registry.js +272 -28
  59. package/dist/core/model-registry.js.map +1 -1
  60. package/dist/core/model-resolver.d.ts.map +1 -1
  61. package/dist/core/model-resolver.js +24 -6
  62. package/dist/core/model-resolver.js.map +1 -1
  63. package/dist/core/output-throughput-history.d.ts +25 -0
  64. package/dist/core/output-throughput-history.d.ts.map +1 -0
  65. package/dist/core/output-throughput-history.js +240 -0
  66. package/dist/core/output-throughput-history.js.map +1 -0
  67. package/dist/core/output-throughput.d.ts +43 -0
  68. package/dist/core/output-throughput.d.ts.map +1 -0
  69. package/dist/core/output-throughput.js +161 -0
  70. package/dist/core/output-throughput.js.map +1 -0
  71. package/dist/core/resource-loader.d.ts +6 -0
  72. package/dist/core/resource-loader.d.ts.map +1 -1
  73. package/dist/core/resource-loader.js +76 -21
  74. package/dist/core/resource-loader.js.map +1 -1
  75. package/dist/core/scramjet-command-parser.d.ts +4 -0
  76. package/dist/core/scramjet-command-parser.d.ts.map +1 -1
  77. package/dist/core/scramjet-command-parser.js +72 -1
  78. package/dist/core/scramjet-command-parser.js.map +1 -1
  79. package/dist/core/sdk.d.ts +4 -0
  80. package/dist/core/sdk.d.ts.map +1 -1
  81. package/dist/core/sdk.js +70 -11
  82. package/dist/core/sdk.js.map +1 -1
  83. package/dist/core/session-manager.d.ts +11 -3
  84. package/dist/core/session-manager.d.ts.map +1 -1
  85. package/dist/core/session-manager.js +46 -11
  86. package/dist/core/session-manager.js.map +1 -1
  87. package/dist/core/settings-manager.d.ts +19 -6
  88. package/dist/core/settings-manager.d.ts.map +1 -1
  89. package/dist/core/settings-manager.js +131 -17
  90. package/dist/core/settings-manager.js.map +1 -1
  91. package/dist/core/skills.d.ts.map +1 -1
  92. package/dist/core/skills.js +6 -2
  93. package/dist/core/skills.js.map +1 -1
  94. package/dist/core/system-prompt.d.ts +2 -0
  95. package/dist/core/system-prompt.d.ts.map +1 -1
  96. package/dist/core/system-prompt.js +5 -2
  97. package/dist/core/system-prompt.js.map +1 -1
  98. package/dist/core/tools/bash.d.ts.map +1 -1
  99. package/dist/core/tools/bash.js +4 -3
  100. package/dist/core/tools/bash.js.map +1 -1
  101. package/dist/index.d.ts +4 -3
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +3 -2
  104. package/dist/index.js.map +1 -1
  105. package/dist/main.d.ts +119 -0
  106. package/dist/main.d.ts.map +1 -1
  107. package/dist/main.js +113 -11
  108. package/dist/main.js.map +1 -1
  109. package/dist/modes/interactive/components/armin.d.ts +4 -2
  110. package/dist/modes/interactive/components/armin.d.ts.map +1 -1
  111. package/dist/modes/interactive/components/armin.js +14 -8
  112. package/dist/modes/interactive/components/armin.js.map +1 -1
  113. package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  114. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  115. package/dist/modes/interactive/components/assistant-message.js +19 -8
  116. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  117. package/dist/modes/interactive/components/daxnuts.d.ts +4 -1
  118. package/dist/modes/interactive/components/daxnuts.d.ts.map +1 -1
  119. package/dist/modes/interactive/components/daxnuts.js +14 -6
  120. package/dist/modes/interactive/components/daxnuts.js.map +1 -1
  121. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  122. package/dist/modes/interactive/components/extension-editor.js +5 -3
  123. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  124. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  125. package/dist/modes/interactive/components/extension-selector.js +4 -3
  126. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  127. package/dist/modes/interactive/components/footer.d.ts +4 -0
  128. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  129. package/dist/modes/interactive/components/footer.js +24 -20
  130. package/dist/modes/interactive/components/footer.js.map +1 -1
  131. package/dist/modes/interactive/components/model-selector.d.ts +3 -3
  132. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  133. package/dist/modes/interactive/components/model-selector.js +10 -7
  134. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  135. package/dist/modes/interactive/components/scoped-models-selector.d.ts +3 -1
  136. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  137. package/dist/modes/interactive/components/scoped-models-selector.js +8 -2
  138. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  139. package/dist/modes/interactive/components/settings-selector.d.ts +17 -3
  140. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  141. package/dist/modes/interactive/components/settings-selector.js +87 -16
  142. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  143. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  144. package/dist/modes/interactive/components/thinking-selector.js +2 -1
  145. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  146. package/dist/modes/interactive/components/tool-execution.d.ts +19 -1
  147. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  148. package/dist/modes/interactive/components/tool-execution.js +129 -36
  149. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  150. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  151. package/dist/modes/interactive/components/user-message.js +3 -2
  152. package/dist/modes/interactive/components/user-message.js.map +1 -1
  153. package/dist/modes/interactive/components/visual-truncate.d.ts.map +1 -1
  154. package/dist/modes/interactive/components/visual-truncate.js +2 -2
  155. package/dist/modes/interactive/components/visual-truncate.js.map +1 -1
  156. package/dist/modes/interactive/interactive-mode.d.ts +37 -1
  157. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  158. package/dist/modes/interactive/interactive-mode.js +902 -177
  159. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  160. package/dist/modes/interactive/spellcheck.d.ts +26 -0
  161. package/dist/modes/interactive/spellcheck.d.ts.map +1 -0
  162. package/dist/modes/interactive/spellcheck.js +144 -0
  163. package/dist/modes/interactive/spellcheck.js.map +1 -0
  164. package/dist/modes/interactive/theme/{dark.json → pi-dark.json} +3 -2
  165. package/dist/modes/interactive/theme/{light.json → pi-light.json} +27 -26
  166. package/dist/modes/interactive/theme/theme-schema.json +6 -1
  167. package/dist/modes/interactive/theme/theme.d.ts +24 -4
  168. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  169. package/dist/modes/interactive/theme/theme.js +123 -22
  170. package/dist/modes/interactive/theme/theme.js.map +1 -1
  171. package/dist/modes/print-mode.d.ts.map +1 -1
  172. package/dist/modes/print-mode.js +5 -0
  173. package/dist/modes/print-mode.js.map +1 -1
  174. package/dist/modes/rpc/rpc-client.d.ts +3 -1
  175. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  176. package/dist/modes/rpc/rpc-client.js.map +1 -1
  177. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  178. package/dist/modes/rpc/rpc-mode.js +6 -1
  179. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  180. package/dist/package-manager-cli.d.ts.map +1 -1
  181. package/dist/package-manager-cli.js +33 -14
  182. package/dist/package-manager-cli.js.map +1 -1
  183. package/dist/utils/clipboard-native.d.ts +1 -0
  184. package/dist/utils/clipboard-native.d.ts.map +1 -1
  185. package/dist/utils/clipboard-native.js.map +1 -1
  186. package/dist/utils/clipboard.d.ts +6 -0
  187. package/dist/utils/clipboard.d.ts.map +1 -1
  188. package/dist/utils/clipboard.js +278 -10
  189. package/dist/utils/clipboard.js.map +1 -1
  190. package/dist/utils/tools-manager.d.ts +1 -1
  191. package/dist/utils/tools-manager.d.ts.map +1 -1
  192. package/dist/utils/tools-manager.js +7 -6
  193. package/dist/utils/tools-manager.js.map +1 -1
  194. package/dist/utils/version-check.d.ts +17 -5
  195. package/dist/utils/version-check.d.ts.map +1 -1
  196. package/dist/utils/version-check.js +88 -21
  197. package/dist/utils/version-check.js.map +1 -1
  198. package/docs/compaction.md +21 -3
  199. package/docs/custom-provider.md +33 -4
  200. package/docs/extensions.md +150 -24
  201. package/docs/json.md +8 -0
  202. package/docs/keybindings.md +25 -1
  203. package/docs/models.md +103 -4
  204. package/docs/packages.md +2 -0
  205. package/docs/prompt-templates.md +2 -0
  206. package/docs/quickstart.md +1 -4
  207. package/docs/rpc.md +16 -6
  208. package/docs/sdk.md +48 -3
  209. package/docs/session-format.md +5 -1
  210. package/docs/settings.md +19 -5
  211. package/docs/skills.md +7 -5
  212. package/docs/terminal-setup.md +79 -2
  213. package/docs/terminal-verification.md +71 -0
  214. package/docs/themes.md +63 -10
  215. package/docs/tui.md +120 -25
  216. package/docs/usage.md +6 -4
  217. package/examples/extensions/border-status-editor.ts +2 -3
  218. package/examples/extensions/doom-overlay/doom-component.ts +3 -3
  219. package/examples/extensions/mac-system-theme.ts +3 -2
  220. package/examples/extensions/modal-editor.ts +13 -8
  221. package/examples/extensions/overlay-qa-tests.ts +22 -9
  222. package/examples/extensions/overlay-test.ts +5 -3
  223. package/examples/extensions/snake.ts +3 -1
  224. package/examples/extensions/space-invaders.ts +6 -3
  225. package/examples/extensions/subagent/index.ts +20 -17
  226. package/package.json +69 -58
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Model registry - manages built-in and custom models, provides API key resolution.
3
3
  */
4
- import { getModels, getProviders, registerApiProvider, resetApiProviders, } from "@leanandmean/ai";
4
+ import { getModels, getProviders, registerApiProvider, resetApiProviders, validateModelContextLimits, } from "@leanandmean/ai";
5
5
  import { registerOAuthProvider, resetOAuthProviders } from "@leanandmean/ai/oauth";
6
6
  import { existsSync, readFileSync } from "fs";
7
7
  import { join } from "path";
@@ -58,7 +58,12 @@ const ThinkingLevelMapSchema = Type.Object({
58
58
  medium: Type.Optional(ThinkingLevelMapValueSchema),
59
59
  high: Type.Optional(ThinkingLevelMapValueSchema),
60
60
  xhigh: Type.Optional(ThinkingLevelMapValueSchema),
61
+ max: Type.Optional(ThinkingLevelMapValueSchema), // SCRAMJET-DIVERGENCE: Match the public thinking-map vocabulary.
61
62
  });
63
+ // SCRAMJET-DIVERGENCE: strict additionalProperties: false on all compat schemas, compat-key derivation,
64
+ // compatKeysForApi/validateCompatForApi/validateProviderCompatForApis, and validateConfig compat loops.
65
+ // Upstream accepts unknown compat keys silently via permissive union; these changes reject invalid and
66
+ // API-incompatible keys at load time. See also the compat-key sets below.
62
67
  const OpenAICompletionsCompatSchema = Type.Object({
63
68
  supportsStore: Type.Optional(Type.Boolean()),
64
69
  supportsDeveloperRole: Type.Optional(Type.Boolean()),
@@ -81,22 +86,39 @@ const OpenAICompletionsCompatSchema = Type.Object({
81
86
  cacheControlFormat: Type.Optional(Type.Literal("anthropic")),
82
87
  openRouterRouting: Type.Optional(OpenRouterRoutingSchema),
83
88
  vercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema),
89
+ zaiToolStream: Type.Optional(Type.Boolean()),
84
90
  supportsStrictMode: Type.Optional(Type.Boolean()),
91
+ sendSessionAffinityHeaders: Type.Optional(Type.Boolean()),
85
92
  supportsLongCacheRetention: Type.Optional(Type.Boolean()),
86
- });
93
+ }, { additionalProperties: false });
87
94
  const OpenAIResponsesCompatSchema = Type.Object({
88
95
  sendSessionIdHeader: Type.Optional(Type.Boolean()),
89
96
  supportsLongCacheRetention: Type.Optional(Type.Boolean()),
90
- });
97
+ }, { additionalProperties: false });
91
98
  const AnthropicMessagesCompatSchema = Type.Object({
92
99
  supportsEagerToolInputStreaming: Type.Optional(Type.Boolean()),
93
100
  supportsLongCacheRetention: Type.Optional(Type.Boolean()),
94
- });
101
+ sendSessionAffinityHeaders: Type.Optional(Type.Boolean()),
102
+ supportsCacheControlOnTools: Type.Optional(Type.Boolean()),
103
+ // SCRAMJET-DIVERGENCE: supportsTemperature and forceAdaptiveThinking for Opus 4.8/Fable 5/Sonnet 5 support
104
+ supportsTemperature: Type.Optional(Type.Boolean()),
105
+ forceAdaptiveThinking: Type.Optional(Type.Boolean()),
106
+ }, { additionalProperties: false });
95
107
  const ProviderCompatSchema = Type.Union([
96
108
  OpenAICompletionsCompatSchema,
97
109
  OpenAIResponsesCompatSchema,
98
110
  AnthropicMessagesCompatSchema,
99
111
  ]);
112
+ // SCRAMJET-DIVERGENCE: compat-key sets derived from their TypeBox schemas to avoid lockstep duplication
113
+ const OPENAI_COMPLETIONS_COMPAT_KEYS = new Set(Object.keys(OpenAICompletionsCompatSchema.properties));
114
+ const OPENAI_RESPONSES_COMPAT_KEYS = new Set(Object.keys(OpenAIResponsesCompatSchema.properties));
115
+ const ANTHROPIC_MESSAGES_COMPAT_KEYS = new Set(Object.keys(AnthropicMessagesCompatSchema.properties));
116
+ const RequestLimitsSchema = Type.Array(Type.Object({
117
+ maxTotalTokens: Type.Number({ exclusiveMinimum: 0 }),
118
+ maxInputTokens: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
119
+ maxOutputTokens: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
120
+ supportsTools: Type.Boolean(),
121
+ }, { additionalProperties: false }), { minItems: 1 });
100
122
  // Schema for custom model definition
101
123
  // Most fields are optional with sensible defaults for local models (Ollama, LM Studio, etc.)
102
124
  const ModelDefinitionSchema = Type.Object({
@@ -114,10 +136,104 @@ const ModelDefinitionSchema = Type.Object({
114
136
  cacheWrite: Type.Number(),
115
137
  })),
116
138
  contextWindow: Type.Optional(Type.Number()),
139
+ maxInputTokens: Type.Optional(Type.Number()),
140
+ requestLimits: Type.Optional(RequestLimitsSchema),
117
141
  maxTokens: Type.Optional(Type.Number()),
118
142
  headers: Type.Optional(Type.Record(Type.String(), Type.String())),
119
143
  compat: Type.Optional(ProviderCompatSchema),
120
144
  });
145
+ // SCRAMJET-DIVERGENCE: Invocation ingress closes reused schemas without tightening persistent configuration.
146
+ function closedInvocationSchema(schema) {
147
+ const result = structuredClone(schema);
148
+ function close(value) {
149
+ if (!value || typeof value !== "object")
150
+ return;
151
+ for (const child of Object.values(value))
152
+ close(child);
153
+ const node = value;
154
+ if (node.type === "object")
155
+ node.additionalProperties = false;
156
+ if (node.type === "number") {
157
+ node.minimum ??= -Number.MAX_VALUE;
158
+ node.maximum ??= Number.MAX_VALUE;
159
+ }
160
+ }
161
+ close(result);
162
+ return result;
163
+ }
164
+ const InvocationModelDefinitionSchema = closedInvocationSchema(Type.Object({
165
+ ...Type.Pick(ModelDefinitionSchema, [
166
+ "id",
167
+ "name",
168
+ "reasoning",
169
+ "input",
170
+ "thinkingLevelMap",
171
+ "compat",
172
+ "cost",
173
+ "maxInputTokens",
174
+ "requestLimits",
175
+ ]).properties,
176
+ api: Type.Union([
177
+ Type.Literal("openai-completions"),
178
+ Type.Literal("openai-responses"),
179
+ Type.Literal("anthropic-messages"),
180
+ Type.Literal("google-generative-ai"),
181
+ ]),
182
+ baseUrl: Type.String({ minLength: 1 }),
183
+ apiKeyEnv: Type.String({ pattern: "^[A-Za-z_][A-Za-z0-9_]*$" }),
184
+ contextWindow: Type.Number({ exclusiveMinimum: 0 }),
185
+ maxTokens: Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }),
186
+ }));
187
+ const validateInvocationDefinition = Compile(InvocationModelDefinitionSchema);
188
+ export function validateInvocationModelDefinition(value) {
189
+ if (!validateInvocationDefinition.Check(value)) {
190
+ const error = validateInvocationDefinition.Errors(value)[0];
191
+ const field = error ? formatValidationPath(error).split(".")[0] : undefined;
192
+ const safeField = field && Object.hasOwn(InvocationModelDefinitionSchema.properties, field) ? field : "object";
193
+ throw new Error(`model definition ${safeField}: supply the required fields and supported metadata with valid values.`);
194
+ }
195
+ if (!value.id.trim())
196
+ throw new Error("model definition id: supply a nonblank literal backend ID.");
197
+ const urlError = "model definition baseUrl: supply an absolute HTTP(S) URL with a host and no credentials, query, fragment, controls or surrounding whitespace.";
198
+ if (value.baseUrl.trim() !== value.baseUrl || /[\s\u0000-\u0020\u007f-\u009f\\?#]/.test(value.baseUrl)) {
199
+ throw new Error(urlError);
200
+ }
201
+ let url;
202
+ try {
203
+ url = new URL(value.baseUrl);
204
+ }
205
+ catch {
206
+ throw new Error(urlError);
207
+ }
208
+ if (!/^https?:\/\//i.test(value.baseUrl) ||
209
+ !["http:", "https:"].includes(url.protocol) ||
210
+ !url.hostname ||
211
+ url.username ||
212
+ url.password ||
213
+ value.baseUrl.split("/")[2].includes("@")) {
214
+ throw new Error(urlError);
215
+ }
216
+ try {
217
+ validateCompatForApi(value.compat, value.api, "compat");
218
+ }
219
+ catch {
220
+ throw new Error("model definition compat: use only compatibility fields supported by the selected API.");
221
+ }
222
+ try {
223
+ validateModelContextLimits({ ...value, provider: "invocation", id: "definition" });
224
+ }
225
+ catch {
226
+ throw new Error("model definition contextWindow/maxInputTokens/requestLimits: supply positive finite applicable limits.");
227
+ }
228
+ // Omitted capabilities and zero price estimates are client metadata, not backend or billing evidence.
229
+ return {
230
+ ...value,
231
+ name: value.name ?? value.id,
232
+ reasoning: value.reasoning ?? false,
233
+ input: value.input ?? ["text"],
234
+ cost: value.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
235
+ };
236
+ }
121
237
  // Schema for per-model overrides (all fields optional, merged with built-in model)
122
238
  const ModelOverrideSchema = Type.Object({
123
239
  name: Type.Optional(Type.String({ minLength: 1 })),
@@ -131,6 +247,8 @@ const ModelOverrideSchema = Type.Object({
131
247
  cacheWrite: Type.Optional(Type.Number()),
132
248
  })),
133
249
  contextWindow: Type.Optional(Type.Number()),
250
+ maxInputTokens: Type.Optional(Type.Number()),
251
+ requestLimits: Type.Optional(RequestLimitsSchema),
134
252
  maxTokens: Type.Optional(Type.Number()),
135
253
  headers: Type.Optional(Type.Record(Type.String(), Type.String())),
136
254
  compat: Type.Optional(ProviderCompatSchema),
@@ -150,6 +268,12 @@ const ModelsConfigSchema = Type.Object({
150
268
  providers: Type.Record(Type.String(), ProviderConfigSchema),
151
269
  });
152
270
  const validateModelsConfig = Compile(ModelsConfigSchema);
271
+ // SCRAMJET-DIVERGENCE: Reject obsolete policy keys rather than silently changing their meaning.
272
+ function rejectContextWindowBudget(model, provider, id) {
273
+ if ("contextWindowBudget" in model) {
274
+ throw new Error(`${provider}/${id}: contextWindowBudget was removed; remove this key and set contextWindow to the evidenced maximum total context. Do not copy a discretionary budget into contextWindow or maxInputTokens.`);
275
+ }
276
+ }
153
277
  function formatValidationPath(error) {
154
278
  if (error.keyword === "required") {
155
279
  const requiredProperties = error.params.requiredProperties;
@@ -171,6 +295,49 @@ function stripJsonComments(input) {
171
295
  function emptyCustomModelsResult(error) {
172
296
  return { models: [], overrides: new Map(), modelOverrides: new Map(), error };
173
297
  }
298
+ function compatKeysForApi(api) {
299
+ switch (api) {
300
+ case "openai-completions":
301
+ return OPENAI_COMPLETIONS_COMPAT_KEYS;
302
+ case "openai-responses":
303
+ return OPENAI_RESPONSES_COMPAT_KEYS;
304
+ case "anthropic-messages":
305
+ return ANTHROPIC_MESSAGES_COMPAT_KEYS;
306
+ default:
307
+ return undefined;
308
+ }
309
+ }
310
+ function validateCompatForApi(compat, api, path) {
311
+ if (!compat)
312
+ return;
313
+ const allowedKeys = compatKeysForApi(api);
314
+ for (const key of Object.keys(compat)) {
315
+ if (!allowedKeys?.has(key)) {
316
+ throw new Error(`${path}.${key}: compat field is not valid for api "${api}".`);
317
+ }
318
+ }
319
+ }
320
+ function validateProviderCompatForApis(compat, apis, path) {
321
+ if (!compat)
322
+ return;
323
+ for (const key of Object.keys(compat)) {
324
+ let validForAny = false;
325
+ for (const api of apis) {
326
+ const allowedKeys = compatKeysForApi(api);
327
+ if (allowedKeys?.has(key)) {
328
+ validForAny = true;
329
+ break;
330
+ }
331
+ }
332
+ if (!validForAny) {
333
+ const apiList = [...apis]
334
+ .sort()
335
+ .map((a) => `"${a}"`)
336
+ .join(", ");
337
+ throw new Error(`${path}.${key}: compat field is not valid for any of the provider's APIs (${apiList}). Use "modelOverrides" to target compat fields to specific models.`);
338
+ }
339
+ }
340
+ }
174
341
  function mergeCompat(baseCompat, overrideCompat) {
175
342
  if (!overrideCompat)
176
343
  return baseCompat;
@@ -198,6 +365,7 @@ function mergeCompat(baseCompat, overrideCompat) {
198
365
  * Deep merge a model override into a model.
199
366
  * Handles nested objects (cost, compat) by merging rather than replacing.
200
367
  */
368
+ // SCRAMJET-DIVERGENCE: Total context and genuine input constraints are independently overridable.
201
369
  function applyModelOverride(model, override) {
202
370
  const result = { ...model };
203
371
  // Simple field overrides
@@ -212,6 +380,10 @@ function applyModelOverride(model, override) {
212
380
  result.input = override.input;
213
381
  if (override.contextWindow !== undefined)
214
382
  result.contextWindow = override.contextWindow;
383
+ if (override.maxInputTokens !== undefined)
384
+ result.maxInputTokens = override.maxInputTokens;
385
+ if (override.requestLimits !== undefined)
386
+ result.requestLimits = override.requestLimits;
215
387
  if (override.maxTokens !== undefined)
216
388
  result.maxTokens = override.maxTokens;
217
389
  // Merge cost (partial override)
@@ -279,13 +451,30 @@ export class ModelRegistry {
279
451
  this.loadError = error;
280
452
  // Keep built-in models even if custom models failed to load
281
453
  }
282
- const builtInModels = this.loadBuiltInModels(overrides, modelOverrides);
454
+ let builtInModels = this.loadBuiltInModels(overrides, modelOverrides);
283
455
  let combined = this.mergeCustomModels(builtInModels, customModels);
456
+ try {
457
+ this.validateContextLimits(combined);
458
+ }
459
+ catch (validationError) {
460
+ this.appendLoadError(`Failed to load models.json: ${validationError instanceof Error ? validationError.message : validationError}`);
461
+ this.providerRequestConfigs.clear();
462
+ this.modelRequestHeaders.clear();
463
+ builtInModels = this.loadBuiltInModels(new Map(), new Map());
464
+ combined = builtInModels;
465
+ }
284
466
  // Let OAuth providers modify their models (e.g., update baseUrl)
285
467
  for (const oauthProvider of this.authStorage.getOAuthProviders()) {
286
468
  const cred = this.authStorage.get(oauthProvider.id);
287
469
  if (cred?.type === "oauth" && oauthProvider.modifyModels) {
288
- combined = oauthProvider.modifyModels(combined, cred);
470
+ try {
471
+ const transformed = oauthProvider.modifyModels(combined, cred);
472
+ this.validateContextLimits(transformed);
473
+ combined = transformed;
474
+ }
475
+ catch (validationError) {
476
+ this.appendLoadError(`Failed to apply OAuth model transform for ${oauthProvider.id}: ${validationError instanceof Error ? validationError.message : validationError}`);
477
+ }
289
478
  }
290
479
  }
291
480
  this.models = combined;
@@ -378,6 +567,8 @@ export class ModelRegistry {
378
567
  const isBuiltIn = builtInProviders.has(providerName);
379
568
  const hasProviderApi = !!providerConfig.api;
380
569
  const models = providerConfig.models ?? [];
570
+ const builtInModels = isBuiltIn ? getModels(providerName) : [];
571
+ const builtInDefaultApi = builtInModels[0]?.api;
381
572
  const hasModelOverrides = providerConfig.modelOverrides && Object.keys(providerConfig.modelOverrides).length > 0;
382
573
  if (models.length === 0) {
383
574
  // Override-only config: needs baseUrl, headers, compat, modelOverrides, or some combination.
@@ -396,8 +587,24 @@ export class ModelRegistry {
396
587
  }
397
588
  // Built-in providers with custom models: baseUrl/apiKey/api are optional,
398
589
  // inherited from built-in models. Auth comes from env vars / auth storage.
399
- for (const modelDef of models) {
590
+ if (providerConfig.compat) {
591
+ const providerCompatApis = new Set();
592
+ for (const model of builtInModels) {
593
+ providerCompatApis.add(model.api);
594
+ }
595
+ for (const modelDef of models) {
596
+ const api = modelDef.api ?? providerConfig.api ?? builtInDefaultApi;
597
+ if (api)
598
+ providerCompatApis.add(api);
599
+ }
600
+ if (!isBuiltIn && providerConfig.api) {
601
+ providerCompatApis.add(providerConfig.api);
602
+ }
603
+ validateProviderCompatForApis(providerConfig.compat, providerCompatApis, `providers.${providerName}.compat`);
604
+ }
605
+ for (const [modelIndex, modelDef] of models.entries()) {
400
606
  const hasModelApi = !!modelDef.api;
607
+ const api = modelDef.api ?? providerConfig.api ?? builtInDefaultApi;
401
608
  if (!hasProviderApi && !hasModelApi && !isBuiltIn) {
402
609
  throw new Error(`Provider ${providerName}, model ${modelDef.id}: no "api" specified. Set at provider or model level.`);
403
610
  }
@@ -407,8 +614,19 @@ export class ModelRegistry {
407
614
  // Validate contextWindow/maxTokens only if provided (they have defaults)
408
615
  if (modelDef.contextWindow !== undefined && modelDef.contextWindow <= 0)
409
616
  throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);
617
+ rejectContextWindowBudget(modelDef, providerName, modelDef.id);
410
618
  if (modelDef.maxTokens !== undefined && modelDef.maxTokens <= 0)
411
619
  throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);
620
+ if (api) {
621
+ validateCompatForApi(modelDef.compat, api, `providers.${providerName}.models[${modelIndex}].compat`);
622
+ }
623
+ }
624
+ for (const [modelId, modelOverride] of Object.entries(providerConfig.modelOverrides ?? {})) {
625
+ rejectContextWindowBudget(modelOverride, providerName, modelId);
626
+ const api = builtInModels.find((model) => model.id === modelId)?.api ?? providerConfig.api ?? builtInDefaultApi;
627
+ if (api) {
628
+ validateCompatForApi(modelOverride.compat, api, `providers.${providerName}.modelOverrides.${modelId}.compat`);
629
+ }
412
630
  }
413
631
  }
414
632
  }
@@ -455,6 +673,8 @@ export class ModelRegistry {
455
673
  input: (modelDef.input ?? ["text"]),
456
674
  cost: modelDef.cost ?? defaultCost,
457
675
  contextWindow: modelDef.contextWindow ?? 128000,
676
+ maxInputTokens: modelDef.maxInputTokens,
677
+ requestLimits: modelDef.requestLimits,
458
678
  maxTokens: modelDef.maxTokens ?? 16384,
459
679
  headers: undefined,
460
680
  compat,
@@ -642,6 +862,15 @@ export class ModelRegistry {
642
862
  }
643
863
  }
644
864
  }
865
+ appendLoadError(error) {
866
+ this.loadError = this.loadError ? `${this.loadError}\n${error}` : error;
867
+ }
868
+ validateContextLimits(models) {
869
+ for (const model of models) {
870
+ validateModelContextLimits(model);
871
+ rejectContextWindowBudget(model, model.provider, model.id);
872
+ }
873
+ }
645
874
  validateProviderConfig(providerName, config) {
646
875
  if (config.streamSimple && !config.api) {
647
876
  throw new Error(`Provider ${providerName}: "api" is required when registering streamSimple.`);
@@ -660,6 +889,9 @@ export class ModelRegistry {
660
889
  if (!api) {
661
890
  throw new Error(`Provider ${providerName}, model ${modelDef.id}: no "api" specified.`);
662
891
  }
892
+ this.validateContextLimits([
893
+ { ...modelDef, api, provider: providerName, baseUrl: modelDef.baseUrl ?? config.baseUrl },
894
+ ]);
663
895
  }
664
896
  }
665
897
  applyProviderConfig(providerName, config) {
@@ -681,15 +913,10 @@ export class ModelRegistry {
681
913
  handlesSystemPromptSections: config.handlesSystemPromptSections,
682
914
  }, `provider:${providerName}`);
683
915
  }
684
- this.storeProviderRequestConfig(providerName, config);
685
916
  if (config.models && config.models.length > 0) {
686
- // Full replacement: remove existing models for this provider
687
- this.models = this.models.filter((m) => m.provider !== providerName);
688
- // Parse and add new models
689
- for (const modelDef of config.models) {
917
+ const providerModels = config.models.map((modelDef) => {
690
918
  const api = modelDef.api || config.api;
691
- this.storeModelHeaders(providerName, modelDef.id, modelDef.headers);
692
- this.models.push({
919
+ return {
693
920
  id: modelDef.id,
694
921
  name: modelDef.name,
695
922
  api: api,
@@ -700,29 +927,46 @@ export class ModelRegistry {
700
927
  input: modelDef.input,
701
928
  cost: modelDef.cost,
702
929
  contextWindow: modelDef.contextWindow,
930
+ maxInputTokens: modelDef.maxInputTokens,
931
+ requestLimits: modelDef.requestLimits,
703
932
  maxTokens: modelDef.maxTokens,
704
933
  headers: undefined,
705
934
  compat: modelDef.compat,
706
- });
707
- }
708
- // Apply OAuth modifyModels if credentials exist (e.g., to update baseUrl)
935
+ };
936
+ });
937
+ let candidateModels = [...this.models.filter((m) => m.provider !== providerName), ...providerModels];
709
938
  if (config.oauth?.modifyModels) {
710
939
  const cred = this.authStorage.get(providerName);
711
940
  if (cred?.type === "oauth") {
712
- this.models = config.oauth.modifyModels(this.models, cred);
941
+ try {
942
+ const transformed = config.oauth.modifyModels(candidateModels, cred);
943
+ this.validateContextLimits(transformed);
944
+ candidateModels = transformed;
945
+ }
946
+ catch (validationError) {
947
+ this.appendLoadError(`Failed to apply OAuth model transform for ${providerName}: ${validationError instanceof Error ? validationError.message : validationError}`);
948
+ }
713
949
  }
714
950
  }
951
+ this.storeProviderRequestConfig(providerName, config);
952
+ for (const modelDef of config.models) {
953
+ this.storeModelHeaders(providerName, modelDef.id, modelDef.headers);
954
+ }
955
+ this.models = candidateModels;
715
956
  }
716
- else if (config.baseUrl || config.headers) {
717
- // Override-only: update baseUrl for existing models. Request headers are resolved per request.
718
- this.models = this.models.map((m) => {
719
- if (m.provider !== providerName)
720
- return m;
721
- return {
722
- ...m,
723
- baseUrl: config.baseUrl ?? m.baseUrl,
724
- };
725
- });
957
+ else {
958
+ this.storeProviderRequestConfig(providerName, config);
959
+ if (config.baseUrl || config.headers) {
960
+ // Override-only: update baseUrl for existing models. Request headers are resolved per request.
961
+ this.models = this.models.map((m) => {
962
+ if (m.provider !== providerName)
963
+ return m;
964
+ return {
965
+ ...m,
966
+ baseUrl: config.baseUrl ?? m.baseUrl,
967
+ };
968
+ });
969
+ }
726
970
  }
727
971
  }
728
972
  }