@gajae-code/coding-agent 0.2.0 → 0.2.1
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 +7 -0
- package/dist/types/cli/skills-cli.d.ts +9 -0
- package/dist/types/commands/skills.d.ts +26 -0
- package/dist/types/config/model-registry.d.ts +31 -2
- package/dist/types/config/models-config-schema.d.ts +39 -0
- package/dist/types/modes/components/model-selector.d.ts +21 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
- package/package.json +7 -7
- package/src/cli/args.ts +14 -0
- package/src/cli/skills-cli.ts +88 -0
- package/src/cli.ts +1 -0
- package/src/commands/skills.ts +48 -0
- package/src/commit/agentic/index.ts +1 -0
- package/src/commit/pipeline.ts +1 -0
- package/src/config/model-registry.ts +259 -8
- package/src/config/models-config-schema.ts +18 -0
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/main.ts +10 -1
- package/src/modes/components/model-selector.ts +109 -28
- package/src/modes/controllers/selector-controller.ts +42 -2
- package/src/sdk.ts +1 -0
- package/src/setup/provider-onboarding.ts +2 -0
- package/src/slash-commands/acp-builtins.ts +11 -2
- package/src/slash-commands/builtin-registry.ts +16 -1
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type Model,
|
|
13
13
|
type ModelManagerOptions,
|
|
14
14
|
type ModelRefreshStrategy,
|
|
15
|
+
type ModelRequestTransform,
|
|
15
16
|
openaiCodexModelManagerOptions,
|
|
16
17
|
PROVIDER_DESCRIPTORS,
|
|
17
18
|
readModelCache,
|
|
@@ -170,11 +171,60 @@ export function getRoleInfo(role: string, settings: Settings): RoleInfo {
|
|
|
170
171
|
|
|
171
172
|
type ProviderValidationMode = "models-config" | "runtime-register";
|
|
172
173
|
|
|
174
|
+
const OPENAI_REQUEST_TRANSFORM_APIS = new Set<Api>(["openai-completions", "openai-responses"]);
|
|
175
|
+
|
|
176
|
+
function getKnownProviderApis(providerName: string): Set<Api> {
|
|
177
|
+
const apis = new Set<Api>();
|
|
178
|
+
for (const model of getBundledModels(providerName as Parameters<typeof getBundledModels>[0])) {
|
|
179
|
+
apis.add((model as Model<Api>).api);
|
|
180
|
+
}
|
|
181
|
+
return apis;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isRequestTransformApi(api: Api): boolean {
|
|
185
|
+
return OPENAI_REQUEST_TRANSFORM_APIS.has(api);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function assertRequestTransformSupportedForKnownProvider(providerName: string, source: string): void {
|
|
189
|
+
const apis = getKnownProviderApis(providerName);
|
|
190
|
+
if (apis.size === 0) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Provider ${providerName}: ${source} requires an OpenAI-compatible "api" when the provider is not built in.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
for (const api of apis) {
|
|
196
|
+
if (!isRequestTransformApi(api)) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`Provider ${providerName}: ${source} is only supported with openai-completions or openai-responses APIs.`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function assertRequestTransformSupportedForModelApi(
|
|
205
|
+
providerName: string,
|
|
206
|
+
modelId: string,
|
|
207
|
+
api: Api,
|
|
208
|
+
source: string,
|
|
209
|
+
): void {
|
|
210
|
+
if (!isRequestTransformApi(api)) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Provider ${providerName}, model ${modelId}: ${source} is only supported with openai-completions or openai-responses APIs.`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function getKnownProviderModelApi(providerName: string, modelId: string): Api | undefined {
|
|
218
|
+
return getBundledModels(providerName as Parameters<typeof getBundledModels>[0]).find(model => model.id === modelId)
|
|
219
|
+
?.api as Api | undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
173
222
|
interface ProviderValidationModel {
|
|
174
223
|
id: string;
|
|
175
224
|
api?: Api;
|
|
176
225
|
contextWindow?: number;
|
|
177
226
|
maxTokens?: number;
|
|
227
|
+
requestTransform?: ModelRequestTransform;
|
|
178
228
|
}
|
|
179
229
|
|
|
180
230
|
interface ProviderValidationConfig {
|
|
@@ -187,6 +237,7 @@ interface ProviderValidationConfig {
|
|
|
187
237
|
oauthConfigured?: boolean;
|
|
188
238
|
discovery?: ProviderDiscovery;
|
|
189
239
|
compat?: Model<Api>["compat"];
|
|
240
|
+
requestTransform?: ModelRequestTransform;
|
|
190
241
|
disableStrictTools?: boolean;
|
|
191
242
|
modelOverrides?: Record<string, unknown>;
|
|
192
243
|
models: ProviderValidationModel[];
|
|
@@ -210,11 +261,12 @@ function validateProviderConfiguration(
|
|
|
210
261
|
!config.apiKey &&
|
|
211
262
|
!config.apiKeyEnv &&
|
|
212
263
|
!config.disableStrictTools &&
|
|
264
|
+
!config.requestTransform &&
|
|
213
265
|
!hasModelOverrides &&
|
|
214
266
|
!config.discovery
|
|
215
267
|
) {
|
|
216
268
|
throw new Error(
|
|
217
|
-
`Provider ${providerName}: must specify "baseUrl", "headers", "apiKey", "compat", "disableStrictTools", "modelOverrides", "discovery", or "models"`,
|
|
269
|
+
`Provider ${providerName}: must specify "baseUrl", "headers", "apiKey", "compat", "requestTransform", "disableStrictTools", "modelOverrides", "discovery", or "models"`,
|
|
218
270
|
);
|
|
219
271
|
}
|
|
220
272
|
}
|
|
@@ -238,6 +290,34 @@ function validateProviderConfiguration(
|
|
|
238
290
|
if (mode === "models-config" && config.discovery && !config.api) {
|
|
239
291
|
throw new Error(`Provider ${providerName}: "api" is required when discovery is enabled at provider level.`);
|
|
240
292
|
}
|
|
293
|
+
for (const [modelId, rawOverride] of Object.entries(config.modelOverrides ?? {})) {
|
|
294
|
+
const override = rawOverride as ModelOverride;
|
|
295
|
+
if (!override.requestTransform) continue;
|
|
296
|
+
const effectiveApi =
|
|
297
|
+
models.find(model => model.id === modelId)?.api ??
|
|
298
|
+
config.api ??
|
|
299
|
+
getKnownProviderModelApi(providerName, modelId);
|
|
300
|
+
if (effectiveApi) {
|
|
301
|
+
assertRequestTransformSupportedForModelApi(
|
|
302
|
+
providerName,
|
|
303
|
+
modelId,
|
|
304
|
+
effectiveApi,
|
|
305
|
+
'modelOverrides "requestTransform"',
|
|
306
|
+
);
|
|
307
|
+
} else {
|
|
308
|
+
assertRequestTransformSupportedForKnownProvider(providerName, 'modelOverrides "requestTransform"');
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (config.requestTransform) {
|
|
312
|
+
if (config.api && !isRequestTransformApi(config.api)) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
`Provider ${providerName}: "requestTransform" is only supported with openai-completions or openai-responses APIs.`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
if (!config.api && models.length === 0) {
|
|
318
|
+
assertRequestTransformSupportedForKnownProvider(providerName, '"requestTransform"');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
241
321
|
|
|
242
322
|
for (const modelDef of models) {
|
|
243
323
|
if (!hasProviderApi && !modelDef.api) {
|
|
@@ -250,6 +330,23 @@ function validateProviderConfiguration(
|
|
|
250
330
|
if (!modelDef.id) {
|
|
251
331
|
throw new Error(`Provider ${providerName}: model missing "id"`);
|
|
252
332
|
}
|
|
333
|
+
const effectiveApi = modelDef.api ?? config.api;
|
|
334
|
+
if (config.requestTransform && effectiveApi) {
|
|
335
|
+
assertRequestTransformSupportedForModelApi(
|
|
336
|
+
providerName,
|
|
337
|
+
modelDef.id,
|
|
338
|
+
effectiveApi,
|
|
339
|
+
'provider "requestTransform"',
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
if (modelDef.requestTransform && effectiveApi) {
|
|
343
|
+
assertRequestTransformSupportedForModelApi(
|
|
344
|
+
providerName,
|
|
345
|
+
modelDef.id,
|
|
346
|
+
effectiveApi,
|
|
347
|
+
'model "requestTransform"',
|
|
348
|
+
);
|
|
349
|
+
}
|
|
253
350
|
if (mode === "models-config") {
|
|
254
351
|
if (modelDef.contextWindow !== undefined && modelDef.contextWindow <= 0) {
|
|
255
352
|
throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);
|
|
@@ -276,6 +373,7 @@ export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsCon
|
|
|
276
373
|
auth: (providerConfig.auth ?? "apiKey") as ProviderAuthMode,
|
|
277
374
|
discovery: providerConfig.discovery as ProviderDiscovery | undefined,
|
|
278
375
|
compat: providerConfig.compat,
|
|
376
|
+
requestTransform: providerConfig.requestTransform,
|
|
279
377
|
disableStrictTools: providerConfig.disableStrictTools,
|
|
280
378
|
modelOverrides: providerConfig.modelOverrides,
|
|
281
379
|
models: (providerConfig.models ?? []) as ProviderValidationModel[],
|
|
@@ -294,6 +392,7 @@ interface ProviderOverride {
|
|
|
294
392
|
authHeader?: boolean;
|
|
295
393
|
compat?: Model<Api>["compat"];
|
|
296
394
|
transport?: Model<Api>["transport"];
|
|
395
|
+
requestTransform?: ModelRequestTransform;
|
|
297
396
|
}
|
|
298
397
|
|
|
299
398
|
const PROVIDER_BASE_URL_ENV_ALIASES: Record<string, readonly string[]> = {
|
|
@@ -341,13 +440,17 @@ function resolveProviderBaseUrlFromEnv(provider: string): string | undefined {
|
|
|
341
440
|
export function mergeDiscoveredModel<TApi extends Api>(
|
|
342
441
|
model: Model<TApi>,
|
|
343
442
|
existing: Model<Api> | undefined,
|
|
344
|
-
providerOverride?: Pick<ProviderOverride, "baseUrl" | "headers" | "transport">,
|
|
443
|
+
providerOverride?: Pick<ProviderOverride, "baseUrl" | "headers" | "transport" | "requestTransform">,
|
|
345
444
|
): Model<TApi> {
|
|
346
445
|
if (existing) {
|
|
347
446
|
return {
|
|
348
447
|
...model,
|
|
349
448
|
baseUrl: providerOverride?.baseUrl ?? model.baseUrl ?? existing.baseUrl,
|
|
350
449
|
headers: existing.headers ? { ...existing.headers, ...model.headers } : model.headers,
|
|
450
|
+
requestTransform: mergeRequestTransform(
|
|
451
|
+
mergeRequestTransform(existing.requestTransform, model.requestTransform),
|
|
452
|
+
providerOverride?.requestTransform,
|
|
453
|
+
),
|
|
351
454
|
};
|
|
352
455
|
}
|
|
353
456
|
if (providerOverride) {
|
|
@@ -356,6 +459,7 @@ export function mergeDiscoveredModel<TApi extends Api>(
|
|
|
356
459
|
baseUrl: providerOverride.baseUrl ?? model.baseUrl,
|
|
357
460
|
headers: providerOverride.headers ? { ...model.headers, ...providerOverride.headers } : model.headers,
|
|
358
461
|
...(providerOverride.transport !== undefined ? { transport: providerOverride.transport } : {}),
|
|
462
|
+
requestTransform: mergeRequestTransform(model.requestTransform, providerOverride.requestTransform),
|
|
359
463
|
};
|
|
360
464
|
}
|
|
361
465
|
return model;
|
|
@@ -367,6 +471,7 @@ interface DiscoveryProviderConfig {
|
|
|
367
471
|
baseUrl?: string;
|
|
368
472
|
headers?: Record<string, string>;
|
|
369
473
|
compat?: Model<Api>["compat"];
|
|
474
|
+
requestTransform?: ModelRequestTransform;
|
|
370
475
|
discovery: ProviderDiscovery;
|
|
371
476
|
optional?: boolean;
|
|
372
477
|
}
|
|
@@ -397,6 +502,7 @@ interface CustomModelsResult {
|
|
|
397
502
|
discoverableProviders?: DiscoveryProviderConfig[];
|
|
398
503
|
configuredProviders?: Set<string>;
|
|
399
504
|
equivalence?: ModelEquivalenceConfig;
|
|
505
|
+
modelBindings?: NonNullable<ModelsConfig["modelBindings"]>;
|
|
400
506
|
error?: ConfigError;
|
|
401
507
|
found: boolean;
|
|
402
508
|
}
|
|
@@ -538,6 +644,24 @@ function mergeCompat<TBase extends object, TOverride extends object>(
|
|
|
538
644
|
return merged as TBase & TOverride;
|
|
539
645
|
}
|
|
540
646
|
|
|
647
|
+
function mergeRequestTransform(
|
|
648
|
+
base: ModelRequestTransform | undefined,
|
|
649
|
+
override: ModelRequestTransform | undefined,
|
|
650
|
+
): ModelRequestTransform | undefined {
|
|
651
|
+
if (!base) return override ? { ...override } : undefined;
|
|
652
|
+
if (!override) return { ...base };
|
|
653
|
+
return {
|
|
654
|
+
...base,
|
|
655
|
+
...override,
|
|
656
|
+
stripHeaders: override.stripHeaders ?? base.stripHeaders,
|
|
657
|
+
setHeaders: override.setHeaders ? { ...(base.setHeaders ?? {}), ...override.setHeaders } : base.setHeaders,
|
|
658
|
+
extraBody:
|
|
659
|
+
base.extraBody || override.extraBody
|
|
660
|
+
? { ...(base.extraBody ?? {}), ...(override.extraBody ?? {}) }
|
|
661
|
+
: undefined,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
541
665
|
function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {
|
|
542
666
|
const result = { ...model };
|
|
543
667
|
if (override.name !== undefined) result.name = override.name;
|
|
@@ -547,6 +671,8 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
|
|
|
547
671
|
if (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;
|
|
548
672
|
if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;
|
|
549
673
|
if (override.contextPromotionTarget !== undefined) result.contextPromotionTarget = override.contextPromotionTarget;
|
|
674
|
+
if (override.wireModelId !== undefined) result.wireModelId = override.wireModelId;
|
|
675
|
+
result.requestTransform = mergeRequestTransform(model.requestTransform, override.requestTransform);
|
|
550
676
|
if (override.premiumMultiplier !== undefined) result.premiumMultiplier = override.premiumMultiplier;
|
|
551
677
|
if (override.cost) {
|
|
552
678
|
result.cost = {
|
|
@@ -578,6 +704,8 @@ interface CustomModelDefinitionLike {
|
|
|
578
704
|
compat?: Model<Api>["compat"];
|
|
579
705
|
contextPromotionTarget?: string;
|
|
580
706
|
premiumMultiplier?: number;
|
|
707
|
+
wireModelId?: string;
|
|
708
|
+
requestTransform?: ModelRequestTransform;
|
|
581
709
|
}
|
|
582
710
|
|
|
583
711
|
interface CustomModelBuildOptions {
|
|
@@ -600,6 +728,8 @@ type CustomModelOverlay = {
|
|
|
600
728
|
compat?: Model<Api>["compat"];
|
|
601
729
|
contextPromotionTarget?: string;
|
|
602
730
|
premiumMultiplier?: number;
|
|
731
|
+
wireModelId?: string;
|
|
732
|
+
requestTransform?: ModelRequestTransform;
|
|
603
733
|
isOAuth?: boolean;
|
|
604
734
|
};
|
|
605
735
|
|
|
@@ -649,6 +779,7 @@ function buildCustomModelOverlay(
|
|
|
649
779
|
providerApiKey: string | undefined,
|
|
650
780
|
authHeader: boolean | undefined,
|
|
651
781
|
providerCompat: Model<Api>["compat"] | undefined,
|
|
782
|
+
providerRequestTransform: ModelRequestTransform | undefined,
|
|
652
783
|
providerAuth: ProviderAuthMode | undefined,
|
|
653
784
|
modelDef: CustomModelDefinitionLike,
|
|
654
785
|
): CustomModelOverlay | undefined {
|
|
@@ -668,6 +799,8 @@ function buildCustomModelOverlay(
|
|
|
668
799
|
maxTokens: modelDef.maxTokens,
|
|
669
800
|
headers: mergeCustomModelHeaders(providerHeaders, modelDef.headers, authHeader, providerApiKey),
|
|
670
801
|
compat: mergeCompat(providerCompat, modelDef.compat),
|
|
802
|
+
requestTransform: mergeRequestTransform(providerRequestTransform, modelDef.requestTransform),
|
|
803
|
+
wireModelId: modelDef.wireModelId,
|
|
671
804
|
contextPromotionTarget: modelDef.contextPromotionTarget,
|
|
672
805
|
premiumMultiplier: modelDef.premiumMultiplier,
|
|
673
806
|
isOAuth: resolveCustomModelIsOAuth(api, providerAuth),
|
|
@@ -768,6 +901,8 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
|
|
|
768
901
|
headers: resolvedModel.headers,
|
|
769
902
|
compat: mergeCompat(reference?.compat, resolvedModel.compat),
|
|
770
903
|
contextPromotionTarget: resolvedModel.contextPromotionTarget,
|
|
904
|
+
wireModelId: resolvedModel.wireModelId,
|
|
905
|
+
requestTransform: resolvedModel.requestTransform,
|
|
771
906
|
premiumMultiplier: resolvedModel.premiumMultiplier,
|
|
772
907
|
isOAuth: resolvedModel.isOAuth,
|
|
773
908
|
} as Model<Api>);
|
|
@@ -810,6 +945,14 @@ export class ModelRegistry {
|
|
|
810
945
|
#providerOverrides: Map<string, ProviderOverride> = new Map();
|
|
811
946
|
#modelOverrides: Map<string, Map<string, ModelOverride>> = new Map();
|
|
812
947
|
#equivalenceConfig: ModelEquivalenceConfig | undefined;
|
|
948
|
+
#configuredModelBindings: NonNullable<ModelsConfig["modelBindings"]> | undefined;
|
|
949
|
+
#modelBindingsTargetSettings: Settings | undefined;
|
|
950
|
+
#appliedModelBindingRoles = new Set<string>();
|
|
951
|
+
#appliedAgentModelBindingOverrides = new Set<string>();
|
|
952
|
+
#modelBindingRoleBaselines = new Map<string, string | undefined>();
|
|
953
|
+
#agentModelBindingBaselines = new Map<string, string | undefined>();
|
|
954
|
+
#lastAppliedModelBindingRoles = new Map<string, string>();
|
|
955
|
+
#lastAppliedAgentModelBindingOverrides = new Map<string, string>();
|
|
813
956
|
#configError: ConfigError | undefined = undefined;
|
|
814
957
|
#modelsConfigFile: ConfigFile<ModelsConfig>;
|
|
815
958
|
#lastStaticLoadMtime: number | null = null;
|
|
@@ -856,6 +999,7 @@ export class ModelRegistry {
|
|
|
856
999
|
this.#reloadStaticModels();
|
|
857
1000
|
this.#suppressedSelectors.clear();
|
|
858
1001
|
await this.#refreshRuntimeDiscoveries(strategy);
|
|
1002
|
+
this.#applyConfiguredModelBindingsToTarget();
|
|
859
1003
|
} finally {
|
|
860
1004
|
this.#resumeRebuild();
|
|
861
1005
|
}
|
|
@@ -889,6 +1033,7 @@ export class ModelRegistry {
|
|
|
889
1033
|
}
|
|
890
1034
|
}
|
|
891
1035
|
await this.#refreshRuntimeDiscoveries(strategy, new Set([providerId]));
|
|
1036
|
+
this.#applyConfiguredModelBindingsToTarget();
|
|
892
1037
|
} finally {
|
|
893
1038
|
this.#resumeRebuild();
|
|
894
1039
|
}
|
|
@@ -916,6 +1061,7 @@ export class ModelRegistry {
|
|
|
916
1061
|
this.#providerOverrides.clear();
|
|
917
1062
|
this.#modelOverrides.clear();
|
|
918
1063
|
this.#equivalenceConfig = undefined;
|
|
1064
|
+
this.#configuredModelBindings = undefined;
|
|
919
1065
|
this.#configError = undefined;
|
|
920
1066
|
this.#providerDiscoveryStates.clear();
|
|
921
1067
|
this.#loadModels();
|
|
@@ -938,6 +1084,7 @@ export class ModelRegistry {
|
|
|
938
1084
|
discoverableProviders = [],
|
|
939
1085
|
configuredProviders = new Set(),
|
|
940
1086
|
equivalence,
|
|
1087
|
+
modelBindings,
|
|
941
1088
|
error: configError,
|
|
942
1089
|
} = this.#loadCustomModels();
|
|
943
1090
|
this.#configError = configError;
|
|
@@ -947,6 +1094,7 @@ export class ModelRegistry {
|
|
|
947
1094
|
this.#providerOverrides = overrides;
|
|
948
1095
|
this.#modelOverrides = modelOverrides;
|
|
949
1096
|
this.#equivalenceConfig = equivalence;
|
|
1097
|
+
this.#configuredModelBindings = modelBindings;
|
|
950
1098
|
|
|
951
1099
|
this.#addImplicitDiscoverableProviders(configuredProviders);
|
|
952
1100
|
const builtInModels = this.#applyHardcodedModelPolicies(this.#loadBuiltInModels(overrides));
|
|
@@ -1044,6 +1192,8 @@ export class ModelRegistry {
|
|
|
1044
1192
|
headers: customModel.headers,
|
|
1045
1193
|
compat: customModel.compat,
|
|
1046
1194
|
contextPromotionTarget: customModel.contextPromotionTarget ?? existingModel.contextPromotionTarget,
|
|
1195
|
+
wireModelId: customModel.wireModelId,
|
|
1196
|
+
requestTransform: customModel.requestTransform,
|
|
1047
1197
|
premiumMultiplier: customModel.premiumMultiplier ?? existingModel.premiumMultiplier,
|
|
1048
1198
|
} as Model<Api>);
|
|
1049
1199
|
} else {
|
|
@@ -1120,11 +1270,20 @@ export class ModelRegistry {
|
|
|
1120
1270
|
}
|
|
1121
1271
|
|
|
1122
1272
|
#normalizeDiscoverableModels(providerConfig: DiscoveryProviderConfig, models: Model<Api>[]): Model<Api>[] {
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1273
|
+
return models.map(model => {
|
|
1274
|
+
const normalized =
|
|
1275
|
+
providerConfig.provider === "ollama" &&
|
|
1276
|
+
providerConfig.api === "openai-responses" &&
|
|
1277
|
+
model.api === "openai-completions"
|
|
1278
|
+
? ({ ...model, api: "openai-responses" } as Model<Api>)
|
|
1279
|
+
: model;
|
|
1280
|
+
return {
|
|
1281
|
+
...normalized,
|
|
1282
|
+
requestTransform: providerConfig.requestTransform
|
|
1283
|
+
? mergeRequestTransform(undefined, providerConfig.requestTransform)
|
|
1284
|
+
: undefined,
|
|
1285
|
+
};
|
|
1286
|
+
});
|
|
1128
1287
|
}
|
|
1129
1288
|
|
|
1130
1289
|
#addImplicitDiscoverableProviders(configuredProviders: Set<string>): void {
|
|
@@ -1208,6 +1367,7 @@ export class ModelRegistry {
|
|
|
1208
1367
|
providerConfig.authHeader !== undefined ||
|
|
1209
1368
|
providerConfig.compat ||
|
|
1210
1369
|
providerConfig.disableStrictTools ||
|
|
1370
|
+
providerConfig.requestTransform ||
|
|
1211
1371
|
providerConfig.transport
|
|
1212
1372
|
) {
|
|
1213
1373
|
const disableStrictCompat = providerConfig.disableStrictTools ? { disableStrictTools: true } : undefined;
|
|
@@ -1218,6 +1378,7 @@ export class ModelRegistry {
|
|
|
1218
1378
|
authHeader: providerConfig.authHeader,
|
|
1219
1379
|
compat: mergeCompat(providerConfig.compat, disableStrictCompat),
|
|
1220
1380
|
transport: providerConfig.transport,
|
|
1381
|
+
requestTransform: providerConfig.requestTransform,
|
|
1221
1382
|
});
|
|
1222
1383
|
}
|
|
1223
1384
|
|
|
@@ -1233,6 +1394,7 @@ export class ModelRegistry {
|
|
|
1233
1394
|
baseUrl: providerConfig.baseUrl ?? resolveProviderBaseUrlFromEnv(providerName),
|
|
1234
1395
|
headers: providerConfig.headers,
|
|
1235
1396
|
compat: providerConfig.compat,
|
|
1397
|
+
requestTransform: providerConfig.requestTransform,
|
|
1236
1398
|
discovery: providerConfig.discovery,
|
|
1237
1399
|
optional: false,
|
|
1238
1400
|
});
|
|
@@ -1270,10 +1432,83 @@ export class ModelRegistry {
|
|
|
1270
1432
|
discoverableProviders,
|
|
1271
1433
|
configuredProviders,
|
|
1272
1434
|
equivalence: value.equivalence,
|
|
1435
|
+
modelBindings: value.modelBindings,
|
|
1273
1436
|
found: true,
|
|
1274
1437
|
};
|
|
1275
1438
|
}
|
|
1276
1439
|
|
|
1440
|
+
applyConfiguredModelBindings(targetSettings: Settings): void {
|
|
1441
|
+
this.#modelBindingsTargetSettings = targetSettings;
|
|
1442
|
+
this.#applyConfiguredModelBindingsToTarget();
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
#applyConfiguredModelBindingsToTarget(): void {
|
|
1446
|
+
const targetSettings = this.#modelBindingsTargetSettings;
|
|
1447
|
+
if (!targetSettings) return;
|
|
1448
|
+
const bindings = this.#configuredModelBindings;
|
|
1449
|
+
const nextModelRoles = { ...targetSettings.get("modelRoles") };
|
|
1450
|
+
const configuredModelRoles = bindings?.modelRoles ?? {};
|
|
1451
|
+
const configuredModelRoleKeys = new Set(Object.keys(configuredModelRoles));
|
|
1452
|
+
for (const role of this.#appliedModelBindingRoles) {
|
|
1453
|
+
if (configuredModelRoleKeys.has(role)) continue;
|
|
1454
|
+
const lastApplied = this.#lastAppliedModelBindingRoles.get(role);
|
|
1455
|
+
if (lastApplied !== undefined && nextModelRoles[role] === lastApplied) {
|
|
1456
|
+
const baseline = this.#modelBindingRoleBaselines.get(role);
|
|
1457
|
+
if (baseline === undefined) {
|
|
1458
|
+
delete nextModelRoles[role];
|
|
1459
|
+
} else {
|
|
1460
|
+
nextModelRoles[role] = baseline;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
this.#modelBindingRoleBaselines.delete(role);
|
|
1464
|
+
this.#lastAppliedModelBindingRoles.delete(role);
|
|
1465
|
+
}
|
|
1466
|
+
for (const [role, modelId] of Object.entries(configuredModelRoles)) {
|
|
1467
|
+
if (!modelId) continue;
|
|
1468
|
+
const previousApplied = this.#lastAppliedModelBindingRoles.get(role);
|
|
1469
|
+
if (!this.#modelBindingRoleBaselines.has(role)) {
|
|
1470
|
+
this.#modelBindingRoleBaselines.set(role, nextModelRoles[role]);
|
|
1471
|
+
}
|
|
1472
|
+
if (previousApplied === undefined || nextModelRoles[role] === previousApplied) {
|
|
1473
|
+
nextModelRoles[role] = modelId;
|
|
1474
|
+
this.#lastAppliedModelBindingRoles.set(role, modelId);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
targetSettings.override("modelRoles", nextModelRoles);
|
|
1478
|
+
this.#appliedModelBindingRoles = new Set(Object.keys(configuredModelRoles));
|
|
1479
|
+
|
|
1480
|
+
const nextAgentModelOverrides = { ...targetSettings.get("task.agentModelOverrides") };
|
|
1481
|
+
const configuredAgentModelOverrides = bindings?.agentModelOverrides ?? {};
|
|
1482
|
+
const configuredAgentModelOverrideKeys = new Set(Object.keys(configuredAgentModelOverrides));
|
|
1483
|
+
for (const agentName of this.#appliedAgentModelBindingOverrides) {
|
|
1484
|
+
if (configuredAgentModelOverrideKeys.has(agentName)) continue;
|
|
1485
|
+
const lastApplied = this.#lastAppliedAgentModelBindingOverrides.get(agentName);
|
|
1486
|
+
if (lastApplied !== undefined && nextAgentModelOverrides[agentName] === lastApplied) {
|
|
1487
|
+
const baseline = this.#agentModelBindingBaselines.get(agentName);
|
|
1488
|
+
if (baseline === undefined) {
|
|
1489
|
+
delete nextAgentModelOverrides[agentName];
|
|
1490
|
+
} else {
|
|
1491
|
+
nextAgentModelOverrides[agentName] = baseline;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
this.#agentModelBindingBaselines.delete(agentName);
|
|
1495
|
+
this.#lastAppliedAgentModelBindingOverrides.delete(agentName);
|
|
1496
|
+
}
|
|
1497
|
+
for (const [agentName, modelId] of Object.entries(configuredAgentModelOverrides)) {
|
|
1498
|
+
if (!modelId) continue;
|
|
1499
|
+
const previousApplied = this.#lastAppliedAgentModelBindingOverrides.get(agentName);
|
|
1500
|
+
if (!this.#agentModelBindingBaselines.has(agentName)) {
|
|
1501
|
+
this.#agentModelBindingBaselines.set(agentName, nextAgentModelOverrides[agentName]);
|
|
1502
|
+
}
|
|
1503
|
+
if (previousApplied === undefined || nextAgentModelOverrides[agentName] === previousApplied) {
|
|
1504
|
+
nextAgentModelOverrides[agentName] = modelId;
|
|
1505
|
+
this.#lastAppliedAgentModelBindingOverrides.set(agentName, modelId);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
targetSettings.override("task.agentModelOverrides", nextAgentModelOverrides);
|
|
1509
|
+
this.#appliedAgentModelBindingOverrides = new Set(Object.keys(configuredAgentModelOverrides));
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1277
1512
|
async #refreshRuntimeDiscoveries(
|
|
1278
1513
|
strategy: ModelRefreshStrategy,
|
|
1279
1514
|
providerFilter?: ReadonlySet<string>,
|
|
@@ -1838,11 +2073,15 @@ export class ModelRegistry {
|
|
|
1838
2073
|
headers: override.headers ? { ...(baseOverride?.headers ?? {}), ...override.headers } : baseOverride?.headers,
|
|
1839
2074
|
compat: override.compat ? mergeCompat(baseOverride?.compat, override.compat) : baseOverride?.compat,
|
|
1840
2075
|
transport: override.transport ?? baseOverride?.transport,
|
|
2076
|
+
requestTransform: mergeRequestTransform(baseOverride?.requestTransform, override.requestTransform),
|
|
1841
2077
|
};
|
|
1842
2078
|
}
|
|
1843
2079
|
#applyProviderTransportOverride<T extends { baseUrl?: string; headers?: Record<string, string> }>(
|
|
1844
2080
|
entry: T,
|
|
1845
|
-
override: Pick<
|
|
2081
|
+
override: Pick<
|
|
2082
|
+
ProviderOverride,
|
|
2083
|
+
"baseUrl" | "headers" | "authHeader" | "apiKey" | "transport" | "requestTransform"
|
|
2084
|
+
>,
|
|
1846
2085
|
): T {
|
|
1847
2086
|
const headers = mergeAuthHeader(
|
|
1848
2087
|
override.headers ? { ...entry.headers, ...override.headers } : entry.headers,
|
|
@@ -1856,6 +2095,10 @@ export class ModelRegistry {
|
|
|
1856
2095
|
// Preserve the model's existing transport when the override omits one;
|
|
1857
2096
|
// providers without a `transport` field keep the default per-API dispatch.
|
|
1858
2097
|
...(override.transport !== undefined ? { transport: override.transport } : {}),
|
|
2098
|
+
requestTransform: mergeRequestTransform(
|
|
2099
|
+
(entry as { requestTransform?: ModelRequestTransform }).requestTransform,
|
|
2100
|
+
override.requestTransform,
|
|
2101
|
+
),
|
|
1859
2102
|
};
|
|
1860
2103
|
}
|
|
1861
2104
|
#applyRuntimeProviderOverrides(models: Model<Api>[]): Model<Api>[] {
|
|
@@ -1942,6 +2185,7 @@ export class ModelRegistry {
|
|
|
1942
2185
|
providerConfig.apiKeyEnv ? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv) : providerConfig.apiKey,
|
|
1943
2186
|
providerConfig.authHeader,
|
|
1944
2187
|
providerCompat,
|
|
2188
|
+
providerConfig.requestTransform,
|
|
1945
2189
|
(providerConfig.auth as ProviderAuthMode | undefined) ?? undefined,
|
|
1946
2190
|
modelDef as CustomModelDefinitionLike,
|
|
1947
2191
|
);
|
|
@@ -2239,6 +2483,7 @@ export class ModelRegistry {
|
|
|
2239
2483
|
apiKey: config.apiKey,
|
|
2240
2484
|
api: config.api,
|
|
2241
2485
|
oauthConfigured: Boolean(config.oauth),
|
|
2486
|
+
requestTransform: config.requestTransform,
|
|
2242
2487
|
models: (config.models ?? []) as ProviderValidationModel[],
|
|
2243
2488
|
},
|
|
2244
2489
|
"runtime-register",
|
|
@@ -2302,6 +2547,7 @@ export class ModelRegistry {
|
|
|
2302
2547
|
config.apiKey,
|
|
2303
2548
|
config.authHeader,
|
|
2304
2549
|
config.compat,
|
|
2550
|
+
config.requestTransform,
|
|
2305
2551
|
undefined,
|
|
2306
2552
|
modelDef as CustomModelDefinitionLike,
|
|
2307
2553
|
);
|
|
@@ -2346,6 +2592,7 @@ export class ModelRegistry {
|
|
|
2346
2592
|
config.headers ||
|
|
2347
2593
|
config.apiKey ||
|
|
2348
2594
|
config.authHeader !== undefined ||
|
|
2595
|
+
config.requestTransform !== undefined ||
|
|
2349
2596
|
config.transport !== undefined
|
|
2350
2597
|
) {
|
|
2351
2598
|
const transportOverride = {
|
|
@@ -2353,6 +2600,7 @@ export class ModelRegistry {
|
|
|
2353
2600
|
headers: config.headers,
|
|
2354
2601
|
apiKey: config.apiKey,
|
|
2355
2602
|
authHeader: config.authHeader,
|
|
2603
|
+
requestTransform: config.requestTransform,
|
|
2356
2604
|
transport: config.transport,
|
|
2357
2605
|
};
|
|
2358
2606
|
const nextRuntimeOverride = this.#mergeProviderOverride(
|
|
@@ -2400,6 +2648,7 @@ export interface ProviderConfigInput {
|
|
|
2400
2648
|
streamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
|
|
2401
2649
|
headers?: Record<string, string>;
|
|
2402
2650
|
compat?: Model<Api>["compat"];
|
|
2651
|
+
requestTransform?: ModelRequestTransform;
|
|
2403
2652
|
authHeader?: boolean;
|
|
2404
2653
|
/** Streaming transport override — see {@link Model.transport}. */
|
|
2405
2654
|
transport?: Model<Api>["transport"];
|
|
@@ -2423,6 +2672,8 @@ export interface ProviderConfigInput {
|
|
|
2423
2672
|
maxTokens: number;
|
|
2424
2673
|
headers?: Record<string, string>;
|
|
2425
2674
|
compat?: Model<Api>["compat"];
|
|
2675
|
+
requestTransform?: ModelRequestTransform;
|
|
2676
|
+
wireModelId?: string;
|
|
2426
2677
|
contextPromotionTarget?: string;
|
|
2427
2678
|
premiumMultiplier?: number;
|
|
2428
2679
|
}>;
|
|
@@ -63,6 +63,18 @@ const ModelThinkingSchema = z.object({
|
|
|
63
63
|
levels: z.array(EffortSchema).optional(),
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
const RequestTransformSchema = z.object({
|
|
67
|
+
profile: z.enum(["openai-proxy"]).optional(),
|
|
68
|
+
stripHeaders: z.array(z.string().min(1)).optional(),
|
|
69
|
+
setHeaders: z.record(z.string(), z.string().nullable()).optional(),
|
|
70
|
+
extraBody: z.record(z.string(), z.unknown()).optional(),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const ModelBindingsSchema = z.object({
|
|
74
|
+
modelRoles: z.record(z.string(), z.string().min(1)).optional(),
|
|
75
|
+
agentModelOverrides: z.record(z.string(), z.string().min(1)).optional(),
|
|
76
|
+
});
|
|
77
|
+
|
|
66
78
|
const ModelDefinitionSchema = z.object({
|
|
67
79
|
id: z.string().min(1),
|
|
68
80
|
name: z.string().min(1).optional(),
|
|
@@ -95,6 +107,8 @@ const ModelDefinitionSchema = z.object({
|
|
|
95
107
|
headers: z.record(z.string(), z.string()).optional(),
|
|
96
108
|
compat: OpenAICompatSchema.optional(),
|
|
97
109
|
contextPromotionTarget: z.string().min(1).optional(),
|
|
110
|
+
wireModelId: z.string().min(1).optional(),
|
|
111
|
+
requestTransform: RequestTransformSchema.optional(),
|
|
98
112
|
});
|
|
99
113
|
|
|
100
114
|
export const ModelOverrideSchema = z.object({
|
|
@@ -116,6 +130,8 @@ export const ModelOverrideSchema = z.object({
|
|
|
116
130
|
headers: z.record(z.string(), z.string()).optional(),
|
|
117
131
|
compat: OpenAICompatSchema.optional(),
|
|
118
132
|
contextPromotionTarget: z.string().min(1).optional(),
|
|
133
|
+
wireModelId: z.string().min(1).optional(),
|
|
134
|
+
requestTransform: RequestTransformSchema.optional(),
|
|
119
135
|
});
|
|
120
136
|
|
|
121
137
|
export type ModelOverride = z.infer<typeof ModelOverrideSchema>;
|
|
@@ -149,6 +165,7 @@ const ProviderConfigSchema = z.object({
|
|
|
149
165
|
authHeader: z.boolean().optional(),
|
|
150
166
|
auth: ProviderAuthSchema.optional(),
|
|
151
167
|
discovery: ProviderDiscoverySchema.optional(),
|
|
168
|
+
requestTransform: RequestTransformSchema.optional(),
|
|
152
169
|
models: z.array(ModelDefinitionSchema).optional(),
|
|
153
170
|
modelOverrides: z.record(z.string(), ModelOverrideSchema).optional(),
|
|
154
171
|
disableStrictTools: z.boolean().optional(),
|
|
@@ -169,6 +186,7 @@ const EquivalenceConfigSchema = z.object({
|
|
|
169
186
|
|
|
170
187
|
export const ModelsConfigSchema = z.object({
|
|
171
188
|
providers: z.record(z.string(), ProviderConfigSchema).optional(),
|
|
189
|
+
modelBindings: ModelBindingsSchema.optional(),
|
|
172
190
|
equivalence: EquivalenceConfigSchema.optional(),
|
|
173
191
|
});
|
|
174
192
|
|