@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.
- package/CHANGELOG.md +32 -0
- package/LICENSE +202 -0
- package/README.md +37 -9
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +45 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/initial-message.d.ts +1 -0
- package/dist/cli/initial-message.d.ts.map +1 -1
- package/dist/cli/initial-message.js +3 -0
- package/dist/cli/initial-message.js.map +1 -1
- package/dist/cli/list-models.d.ts +2 -0
- package/dist/cli/list-models.d.ts.map +1 -1
- package/dist/cli/list-models.js +4 -1
- package/dist/cli/list-models.js.map +1 -1
- package/dist/config.d.ts +9 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +33 -22
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts +12 -3
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +64 -38
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +2 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +3 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +84 -17
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1082 -209
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +21 -17
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +3 -5
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +43 -10
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/exec.d.ts +12 -0
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +74 -28
- package/dist/core/exec.js.map +1 -1
- package/dist/core/export-html/template.js +4 -0
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +9 -5
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +160 -3
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +85 -8
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/keybindings.d.ts +8 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/model-registry.d.ts +103 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +272 -28
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +24 -6
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/output-throughput-history.d.ts +25 -0
- package/dist/core/output-throughput-history.d.ts.map +1 -0
- package/dist/core/output-throughput-history.js +240 -0
- package/dist/core/output-throughput-history.js.map +1 -0
- package/dist/core/output-throughput.d.ts +43 -0
- package/dist/core/output-throughput.d.ts.map +1 -0
- package/dist/core/output-throughput.js +161 -0
- package/dist/core/output-throughput.js.map +1 -0
- package/dist/core/resource-loader.d.ts +6 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +76 -21
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/scramjet-command-parser.d.ts +4 -0
- package/dist/core/scramjet-command-parser.d.ts.map +1 -1
- package/dist/core/scramjet-command-parser.js +72 -1
- package/dist/core/scramjet-command-parser.js.map +1 -1
- package/dist/core/sdk.d.ts +4 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +70 -11
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +11 -3
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +46 -11
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +19 -6
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +131 -17
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +6 -2
- package/dist/core/skills.js.map +1 -1
- package/dist/core/system-prompt.d.ts +2 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +5 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +4 -3
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +119 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +113 -11
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/armin.d.ts +4 -2
- package/dist/modes/interactive/components/armin.d.ts.map +1 -1
- package/dist/modes/interactive/components/armin.js +14 -8
- package/dist/modes/interactive/components/armin.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +19 -8
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/daxnuts.d.ts +4 -1
- package/dist/modes/interactive/components/daxnuts.d.ts.map +1 -1
- package/dist/modes/interactive/components/daxnuts.js +14 -6
- package/dist/modes/interactive/components/daxnuts.js.map +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +5 -3
- package/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +4 -3
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +4 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +24 -20
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +3 -3
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +10 -7
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts +3 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +8 -2
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +17 -3
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +87 -16
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +2 -1
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +19 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +129 -36
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +3 -2
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/components/visual-truncate.d.ts.map +1 -1
- package/dist/modes/interactive/components/visual-truncate.js +2 -2
- package/dist/modes/interactive/components/visual-truncate.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +37 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +902 -177
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/spellcheck.d.ts +26 -0
- package/dist/modes/interactive/spellcheck.d.ts.map +1 -0
- package/dist/modes/interactive/spellcheck.js +144 -0
- package/dist/modes/interactive/spellcheck.js.map +1 -0
- package/dist/modes/interactive/theme/{dark.json → pi-dark.json} +3 -2
- package/dist/modes/interactive/theme/{light.json → pi-light.json} +27 -26
- package/dist/modes/interactive/theme/theme-schema.json +6 -1
- package/dist/modes/interactive/theme/theme.d.ts +24 -4
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +123 -22
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +5 -0
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +3 -1
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +6 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +33 -14
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/clipboard-native.d.ts +1 -0
- package/dist/utils/clipboard-native.d.ts.map +1 -1
- package/dist/utils/clipboard-native.js.map +1 -1
- package/dist/utils/clipboard.d.ts +6 -0
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +278 -10
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +7 -6
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/version-check.d.ts +17 -5
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +88 -21
- package/dist/utils/version-check.js.map +1 -1
- package/docs/compaction.md +21 -3
- package/docs/custom-provider.md +33 -4
- package/docs/extensions.md +150 -24
- package/docs/json.md +8 -0
- package/docs/keybindings.md +25 -1
- package/docs/models.md +103 -4
- package/docs/packages.md +2 -0
- package/docs/prompt-templates.md +2 -0
- package/docs/quickstart.md +1 -4
- package/docs/rpc.md +16 -6
- package/docs/sdk.md +48 -3
- package/docs/session-format.md +5 -1
- package/docs/settings.md +19 -5
- package/docs/skills.md +7 -5
- package/docs/terminal-setup.md +79 -2
- package/docs/terminal-verification.md +71 -0
- package/docs/themes.md +63 -10
- package/docs/tui.md +120 -25
- package/docs/usage.md +6 -4
- package/examples/extensions/border-status-editor.ts +2 -3
- package/examples/extensions/doom-overlay/doom-component.ts +3 -3
- package/examples/extensions/mac-system-theme.ts +3 -2
- package/examples/extensions/modal-editor.ts +13 -8
- package/examples/extensions/overlay-qa-tests.ts +22 -9
- package/examples/extensions/overlay-test.ts +5 -3
- package/examples/extensions/snake.ts +3 -1
- package/examples/extensions/space-invaders.ts +6 -3
- package/examples/extensions/subagent/index.ts +20 -17
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
}
|