@gajae-code/coding-agent 0.2.0 → 0.2.2

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 (114) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/dist/types/cli/skills-cli.d.ts +9 -0
  3. package/dist/types/commands/contribution-prep.d.ts +18 -0
  4. package/dist/types/commands/session.d.ts +24 -0
  5. package/dist/types/commands/skills.d.ts +26 -0
  6. package/dist/types/config/model-registry.d.ts +33 -4
  7. package/dist/types/config/models-config-schema.d.ts +52 -5
  8. package/dist/types/config/settings-schema.d.ts +1 -24
  9. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +15 -0
  10. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  11. package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -11
  12. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +25 -0
  13. package/dist/types/gjc-runtime/state-runtime.d.ts +13 -0
  14. package/dist/types/gjc-runtime/team-runtime.d.ts +37 -5
  15. package/dist/types/gjc-runtime/tmux-common.d.ts +41 -0
  16. package/dist/types/gjc-runtime/tmux-sessions.d.ts +17 -0
  17. package/dist/types/goals/runtime.d.ts +3 -9
  18. package/dist/types/goals/state.d.ts +3 -6
  19. package/dist/types/goals/tools/goal-tool.d.ts +1 -69
  20. package/dist/types/modes/components/model-selector.d.ts +21 -1
  21. package/dist/types/modes/components/status-line/types.d.ts +0 -3
  22. package/dist/types/modes/components/status-line.d.ts +0 -3
  23. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  24. package/dist/types/modes/interactive-mode.d.ts +1 -12
  25. package/dist/types/modes/theme/defaults/index.d.ts +0 -2
  26. package/dist/types/modes/theme/theme.d.ts +1 -2
  27. package/dist/types/modes/types.d.ts +1 -7
  28. package/dist/types/session/agent-session.d.ts +2 -0
  29. package/dist/types/session/contribution-prep.d.ts +47 -0
  30. package/dist/types/skill-state/active-state.d.ts +4 -0
  31. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +6 -1
  32. package/dist/types/skill-state/workflow-hud.d.ts +9 -4
  33. package/dist/types/skill-state/workflow-state-contract.d.ts +34 -0
  34. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  35. package/package.json +7 -7
  36. package/src/cli/args.ts +17 -2
  37. package/src/cli/skills-cli.ts +88 -0
  38. package/src/cli.ts +7 -1
  39. package/src/commands/contribution-prep.ts +41 -0
  40. package/src/commands/deep-interview.ts +6 -22
  41. package/src/commands/launch.ts +10 -1
  42. package/src/commands/ralplan.ts +10 -22
  43. package/src/commands/session.ts +150 -0
  44. package/src/commands/skills.ts +48 -0
  45. package/src/commands/state.ts +14 -4
  46. package/src/commands/team.ts +23 -3
  47. package/src/commit/agentic/index.ts +1 -0
  48. package/src/commit/pipeline.ts +1 -0
  49. package/src/config/model-registry.ts +269 -10
  50. package/src/config/models-config-schema.ts +124 -88
  51. package/src/config/settings-schema.ts +1 -25
  52. package/src/config.ts +1 -1
  53. package/src/defaults/gjc/skills/deep-interview/SKILL.md +14 -13
  54. package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -2
  55. package/src/defaults/gjc/skills/team/SKILL.md +29 -7
  56. package/src/defaults/gjc/skills/ultragoal/SKILL.md +23 -25
  57. package/src/eval/py/prelude.py +1 -1
  58. package/src/gjc-runtime/deep-interview-runtime.ts +279 -0
  59. package/src/gjc-runtime/goal-mode-request.ts +2 -19
  60. package/src/gjc-runtime/launch-tmux.ts +83 -43
  61. package/src/gjc-runtime/ralplan-runtime.ts +460 -0
  62. package/src/gjc-runtime/state-runtime.ts +562 -0
  63. package/src/gjc-runtime/team-runtime.ts +708 -52
  64. package/src/gjc-runtime/tmux-common.ts +119 -0
  65. package/src/gjc-runtime/tmux-sessions.ts +165 -0
  66. package/src/gjc-runtime/ultragoal-guard.ts +6 -3
  67. package/src/gjc-runtime/ultragoal-runtime.ts +5 -4
  68. package/src/goals/runtime.ts +38 -144
  69. package/src/goals/state.ts +36 -7
  70. package/src/goals/tools/goal-tool.ts +15 -172
  71. package/src/hooks/skill-state.ts +31 -12
  72. package/src/internal-urls/docs-index.generated.ts +4 -3
  73. package/src/main.ts +10 -1
  74. package/src/modes/components/model-selector.ts +109 -28
  75. package/src/modes/components/skill-hud/render.ts +4 -0
  76. package/src/modes/components/status-line/segments.ts +5 -16
  77. package/src/modes/components/status-line/types.ts +0 -3
  78. package/src/modes/components/status-line.ts +0 -6
  79. package/src/modes/controllers/command-controller.ts +25 -1
  80. package/src/modes/controllers/input-controller.ts +0 -15
  81. package/src/modes/controllers/selector-controller.ts +42 -2
  82. package/src/modes/interactive-mode.ts +18 -219
  83. package/src/modes/theme/defaults/dark-poimandres.json +0 -1
  84. package/src/modes/theme/defaults/light-poimandres.json +0 -1
  85. package/src/modes/theme/theme.ts +0 -6
  86. package/src/modes/types.ts +1 -7
  87. package/src/prompts/goals/goal-continuation.md +1 -4
  88. package/src/prompts/goals/goal-mode-active.md +3 -5
  89. package/src/prompts/system/system-prompt.md +5 -7
  90. package/src/prompts/tools/goal.md +4 -4
  91. package/src/sdk.ts +2 -1
  92. package/src/session/agent-session.ts +18 -0
  93. package/src/session/contribution-prep.ts +320 -0
  94. package/src/setup/provider-onboarding.ts +2 -0
  95. package/src/skill-state/active-state.ts +38 -0
  96. package/src/skill-state/deep-interview-mutation-guard.ts +88 -24
  97. package/src/skill-state/workflow-hud.ts +23 -5
  98. package/src/skill-state/workflow-state-contract.ts +121 -0
  99. package/src/slash-commands/acp-builtins.ts +11 -2
  100. package/src/slash-commands/builtin-registry.ts +40 -13
  101. package/src/task/commands.ts +1 -5
  102. package/src/tools/gh.ts +212 -2
  103. package/src/tools/index.ts +2 -5
  104. package/dist/types/commands/gjc-runtime-bridge.d.ts +0 -30
  105. package/dist/types/commands/question.d.ts +0 -7
  106. package/dist/types/modes/loop-limit.d.ts +0 -22
  107. package/src/commands/gjc-runtime-bridge.ts +0 -227
  108. package/src/commands/question.ts +0 -12
  109. package/src/modes/loop-limit.ts +0 -140
  110. package/src/prompts/commands/orchestrate.md +0 -49
  111. package/src/prompts/goals/goal-budget-limit.md +0 -16
  112. package/src/prompts/tools/create-goal.md +0 -3
  113. package/src/prompts/tools/get-goal.md +0 -3
  114. package/src/prompts/tools/update-goal.md +0 -3
@@ -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,11 +237,16 @@ 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[];
193
244
  }
194
245
 
246
+ function usesAwsCredentialChain(api: Api | undefined): boolean {
247
+ return api === "bedrock-converse-stream";
248
+ }
249
+
195
250
  function validateProviderConfiguration(
196
251
  providerName: string,
197
252
  config: ProviderValidationConfig,
@@ -210,11 +265,12 @@ function validateProviderConfiguration(
210
265
  !config.apiKey &&
211
266
  !config.apiKeyEnv &&
212
267
  !config.disableStrictTools &&
268
+ !config.requestTransform &&
213
269
  !hasModelOverrides &&
214
270
  !config.discovery
215
271
  ) {
216
272
  throw new Error(
217
- `Provider ${providerName}: must specify "baseUrl", "headers", "apiKey", "compat", "disableStrictTools", "modelOverrides", "discovery", or "models"`,
273
+ `Provider ${providerName}: must specify "baseUrl", "headers", "apiKey", "compat", "requestTransform", "disableStrictTools", "modelOverrides", "discovery", or "models"`,
218
274
  );
219
275
  }
220
276
  }
@@ -222,10 +278,14 @@ function validateProviderConfiguration(
222
278
  if (!config.baseUrl) {
223
279
  throw new Error(`Provider ${providerName}: "baseUrl" is required when defining custom models.`);
224
280
  }
281
+ const usesProviderCredentialChain = usesAwsCredentialChain(config.api);
225
282
  const requiresAuth =
226
283
  mode === "runtime-register"
227
- ? !config.apiKey && !config.oauthConfigured
228
- : !config.apiKey && !config.apiKeyEnv && (config.auth ?? "apiKey") !== "none";
284
+ ? !usesProviderCredentialChain && !config.apiKey && !config.oauthConfigured
285
+ : !usesProviderCredentialChain &&
286
+ !config.apiKey &&
287
+ !config.apiKeyEnv &&
288
+ (config.auth ?? "apiKey") !== "none";
229
289
  if (requiresAuth) {
230
290
  throw new Error(
231
291
  mode === "runtime-register"
@@ -238,6 +298,34 @@ function validateProviderConfiguration(
238
298
  if (mode === "models-config" && config.discovery && !config.api) {
239
299
  throw new Error(`Provider ${providerName}: "api" is required when discovery is enabled at provider level.`);
240
300
  }
301
+ for (const [modelId, rawOverride] of Object.entries(config.modelOverrides ?? {})) {
302
+ const override = rawOverride as ModelOverride;
303
+ if (!override.requestTransform) continue;
304
+ const effectiveApi =
305
+ models.find(model => model.id === modelId)?.api ??
306
+ config.api ??
307
+ getKnownProviderModelApi(providerName, modelId);
308
+ if (effectiveApi) {
309
+ assertRequestTransformSupportedForModelApi(
310
+ providerName,
311
+ modelId,
312
+ effectiveApi,
313
+ 'modelOverrides "requestTransform"',
314
+ );
315
+ } else {
316
+ assertRequestTransformSupportedForKnownProvider(providerName, 'modelOverrides "requestTransform"');
317
+ }
318
+ }
319
+ if (config.requestTransform) {
320
+ if (config.api && !isRequestTransformApi(config.api)) {
321
+ throw new Error(
322
+ `Provider ${providerName}: "requestTransform" is only supported with openai-completions or openai-responses APIs.`,
323
+ );
324
+ }
325
+ if (!config.api && models.length === 0) {
326
+ assertRequestTransformSupportedForKnownProvider(providerName, '"requestTransform"');
327
+ }
328
+ }
241
329
 
242
330
  for (const modelDef of models) {
243
331
  if (!hasProviderApi && !modelDef.api) {
@@ -250,6 +338,23 @@ function validateProviderConfiguration(
250
338
  if (!modelDef.id) {
251
339
  throw new Error(`Provider ${providerName}: model missing "id"`);
252
340
  }
341
+ const effectiveApi = modelDef.api ?? config.api;
342
+ if (config.requestTransform && effectiveApi) {
343
+ assertRequestTransformSupportedForModelApi(
344
+ providerName,
345
+ modelDef.id,
346
+ effectiveApi,
347
+ 'provider "requestTransform"',
348
+ );
349
+ }
350
+ if (modelDef.requestTransform && effectiveApi) {
351
+ assertRequestTransformSupportedForModelApi(
352
+ providerName,
353
+ modelDef.id,
354
+ effectiveApi,
355
+ 'model "requestTransform"',
356
+ );
357
+ }
253
358
  if (mode === "models-config") {
254
359
  if (modelDef.contextWindow !== undefined && modelDef.contextWindow <= 0) {
255
360
  throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);
@@ -276,6 +381,7 @@ export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsCon
276
381
  auth: (providerConfig.auth ?? "apiKey") as ProviderAuthMode,
277
382
  discovery: providerConfig.discovery as ProviderDiscovery | undefined,
278
383
  compat: providerConfig.compat,
384
+ requestTransform: providerConfig.requestTransform,
279
385
  disableStrictTools: providerConfig.disableStrictTools,
280
386
  modelOverrides: providerConfig.modelOverrides,
281
387
  models: (providerConfig.models ?? []) as ProviderValidationModel[],
@@ -294,6 +400,7 @@ interface ProviderOverride {
294
400
  authHeader?: boolean;
295
401
  compat?: Model<Api>["compat"];
296
402
  transport?: Model<Api>["transport"];
403
+ requestTransform?: ModelRequestTransform;
297
404
  }
298
405
 
299
406
  const PROVIDER_BASE_URL_ENV_ALIASES: Record<string, readonly string[]> = {
@@ -341,13 +448,17 @@ function resolveProviderBaseUrlFromEnv(provider: string): string | undefined {
341
448
  export function mergeDiscoveredModel<TApi extends Api>(
342
449
  model: Model<TApi>,
343
450
  existing: Model<Api> | undefined,
344
- providerOverride?: Pick<ProviderOverride, "baseUrl" | "headers" | "transport">,
451
+ providerOverride?: Pick<ProviderOverride, "baseUrl" | "headers" | "transport" | "requestTransform">,
345
452
  ): Model<TApi> {
346
453
  if (existing) {
347
454
  return {
348
455
  ...model,
349
456
  baseUrl: providerOverride?.baseUrl ?? model.baseUrl ?? existing.baseUrl,
350
457
  headers: existing.headers ? { ...existing.headers, ...model.headers } : model.headers,
458
+ requestTransform: mergeRequestTransform(
459
+ mergeRequestTransform(existing.requestTransform, model.requestTransform),
460
+ providerOverride?.requestTransform,
461
+ ),
351
462
  };
352
463
  }
353
464
  if (providerOverride) {
@@ -356,6 +467,7 @@ export function mergeDiscoveredModel<TApi extends Api>(
356
467
  baseUrl: providerOverride.baseUrl ?? model.baseUrl,
357
468
  headers: providerOverride.headers ? { ...model.headers, ...providerOverride.headers } : model.headers,
358
469
  ...(providerOverride.transport !== undefined ? { transport: providerOverride.transport } : {}),
470
+ requestTransform: mergeRequestTransform(model.requestTransform, providerOverride.requestTransform),
359
471
  };
360
472
  }
361
473
  return model;
@@ -367,6 +479,7 @@ interface DiscoveryProviderConfig {
367
479
  baseUrl?: string;
368
480
  headers?: Record<string, string>;
369
481
  compat?: Model<Api>["compat"];
482
+ requestTransform?: ModelRequestTransform;
370
483
  discovery: ProviderDiscovery;
371
484
  optional?: boolean;
372
485
  }
@@ -397,6 +510,7 @@ interface CustomModelsResult {
397
510
  discoverableProviders?: DiscoveryProviderConfig[];
398
511
  configuredProviders?: Set<string>;
399
512
  equivalence?: ModelEquivalenceConfig;
513
+ modelBindings?: NonNullable<ModelsConfig["modelBindings"]>;
400
514
  error?: ConfigError;
401
515
  found: boolean;
402
516
  }
@@ -538,6 +652,24 @@ function mergeCompat<TBase extends object, TOverride extends object>(
538
652
  return merged as TBase & TOverride;
539
653
  }
540
654
 
655
+ function mergeRequestTransform(
656
+ base: ModelRequestTransform | undefined,
657
+ override: ModelRequestTransform | undefined,
658
+ ): ModelRequestTransform | undefined {
659
+ if (!base) return override ? { ...override } : undefined;
660
+ if (!override) return { ...base };
661
+ return {
662
+ ...base,
663
+ ...override,
664
+ stripHeaders: override.stripHeaders ?? base.stripHeaders,
665
+ setHeaders: override.setHeaders ? { ...(base.setHeaders ?? {}), ...override.setHeaders } : base.setHeaders,
666
+ extraBody:
667
+ base.extraBody || override.extraBody
668
+ ? { ...(base.extraBody ?? {}), ...(override.extraBody ?? {}) }
669
+ : undefined,
670
+ };
671
+ }
672
+
541
673
  function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<Api> {
542
674
  const result = { ...model };
543
675
  if (override.name !== undefined) result.name = override.name;
@@ -547,6 +679,8 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
547
679
  if (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;
548
680
  if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;
549
681
  if (override.contextPromotionTarget !== undefined) result.contextPromotionTarget = override.contextPromotionTarget;
682
+ if (override.wireModelId !== undefined) result.wireModelId = override.wireModelId;
683
+ result.requestTransform = mergeRequestTransform(model.requestTransform, override.requestTransform);
550
684
  if (override.premiumMultiplier !== undefined) result.premiumMultiplier = override.premiumMultiplier;
551
685
  if (override.cost) {
552
686
  result.cost = {
@@ -578,6 +712,8 @@ interface CustomModelDefinitionLike {
578
712
  compat?: Model<Api>["compat"];
579
713
  contextPromotionTarget?: string;
580
714
  premiumMultiplier?: number;
715
+ wireModelId?: string;
716
+ requestTransform?: ModelRequestTransform;
581
717
  }
582
718
 
583
719
  interface CustomModelBuildOptions {
@@ -600,6 +736,8 @@ type CustomModelOverlay = {
600
736
  compat?: Model<Api>["compat"];
601
737
  contextPromotionTarget?: string;
602
738
  premiumMultiplier?: number;
739
+ wireModelId?: string;
740
+ requestTransform?: ModelRequestTransform;
603
741
  isOAuth?: boolean;
604
742
  };
605
743
 
@@ -649,6 +787,7 @@ function buildCustomModelOverlay(
649
787
  providerApiKey: string | undefined,
650
788
  authHeader: boolean | undefined,
651
789
  providerCompat: Model<Api>["compat"] | undefined,
790
+ providerRequestTransform: ModelRequestTransform | undefined,
652
791
  providerAuth: ProviderAuthMode | undefined,
653
792
  modelDef: CustomModelDefinitionLike,
654
793
  ): CustomModelOverlay | undefined {
@@ -668,6 +807,8 @@ function buildCustomModelOverlay(
668
807
  maxTokens: modelDef.maxTokens,
669
808
  headers: mergeCustomModelHeaders(providerHeaders, modelDef.headers, authHeader, providerApiKey),
670
809
  compat: mergeCompat(providerCompat, modelDef.compat),
810
+ requestTransform: mergeRequestTransform(providerRequestTransform, modelDef.requestTransform),
811
+ wireModelId: modelDef.wireModelId,
671
812
  contextPromotionTarget: modelDef.contextPromotionTarget,
672
813
  premiumMultiplier: modelDef.premiumMultiplier,
673
814
  isOAuth: resolveCustomModelIsOAuth(api, providerAuth),
@@ -768,6 +909,8 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
768
909
  headers: resolvedModel.headers,
769
910
  compat: mergeCompat(reference?.compat, resolvedModel.compat),
770
911
  contextPromotionTarget: resolvedModel.contextPromotionTarget,
912
+ wireModelId: resolvedModel.wireModelId,
913
+ requestTransform: resolvedModel.requestTransform,
771
914
  premiumMultiplier: resolvedModel.premiumMultiplier,
772
915
  isOAuth: resolvedModel.isOAuth,
773
916
  } as Model<Api>);
@@ -810,6 +953,14 @@ export class ModelRegistry {
810
953
  #providerOverrides: Map<string, ProviderOverride> = new Map();
811
954
  #modelOverrides: Map<string, Map<string, ModelOverride>> = new Map();
812
955
  #equivalenceConfig: ModelEquivalenceConfig | undefined;
956
+ #configuredModelBindings: NonNullable<ModelsConfig["modelBindings"]> | undefined;
957
+ #modelBindingsTargetSettings: Settings | undefined;
958
+ #appliedModelBindingRoles = new Set<string>();
959
+ #appliedAgentModelBindingOverrides = new Set<string>();
960
+ #modelBindingRoleBaselines = new Map<string, string | undefined>();
961
+ #agentModelBindingBaselines = new Map<string, string | undefined>();
962
+ #lastAppliedModelBindingRoles = new Map<string, string>();
963
+ #lastAppliedAgentModelBindingOverrides = new Map<string, string>();
813
964
  #configError: ConfigError | undefined = undefined;
814
965
  #modelsConfigFile: ConfigFile<ModelsConfig>;
815
966
  #lastStaticLoadMtime: number | null = null;
@@ -856,6 +1007,7 @@ export class ModelRegistry {
856
1007
  this.#reloadStaticModels();
857
1008
  this.#suppressedSelectors.clear();
858
1009
  await this.#refreshRuntimeDiscoveries(strategy);
1010
+ this.#applyConfiguredModelBindingsToTarget();
859
1011
  } finally {
860
1012
  this.#resumeRebuild();
861
1013
  }
@@ -889,6 +1041,7 @@ export class ModelRegistry {
889
1041
  }
890
1042
  }
891
1043
  await this.#refreshRuntimeDiscoveries(strategy, new Set([providerId]));
1044
+ this.#applyConfiguredModelBindingsToTarget();
892
1045
  } finally {
893
1046
  this.#resumeRebuild();
894
1047
  }
@@ -916,6 +1069,7 @@ export class ModelRegistry {
916
1069
  this.#providerOverrides.clear();
917
1070
  this.#modelOverrides.clear();
918
1071
  this.#equivalenceConfig = undefined;
1072
+ this.#configuredModelBindings = undefined;
919
1073
  this.#configError = undefined;
920
1074
  this.#providerDiscoveryStates.clear();
921
1075
  this.#loadModels();
@@ -938,6 +1092,7 @@ export class ModelRegistry {
938
1092
  discoverableProviders = [],
939
1093
  configuredProviders = new Set(),
940
1094
  equivalence,
1095
+ modelBindings,
941
1096
  error: configError,
942
1097
  } = this.#loadCustomModels();
943
1098
  this.#configError = configError;
@@ -947,6 +1102,7 @@ export class ModelRegistry {
947
1102
  this.#providerOverrides = overrides;
948
1103
  this.#modelOverrides = modelOverrides;
949
1104
  this.#equivalenceConfig = equivalence;
1105
+ this.#configuredModelBindings = modelBindings;
950
1106
 
951
1107
  this.#addImplicitDiscoverableProviders(configuredProviders);
952
1108
  const builtInModels = this.#applyHardcodedModelPolicies(this.#loadBuiltInModels(overrides));
@@ -1044,6 +1200,8 @@ export class ModelRegistry {
1044
1200
  headers: customModel.headers,
1045
1201
  compat: customModel.compat,
1046
1202
  contextPromotionTarget: customModel.contextPromotionTarget ?? existingModel.contextPromotionTarget,
1203
+ wireModelId: customModel.wireModelId,
1204
+ requestTransform: customModel.requestTransform,
1047
1205
  premiumMultiplier: customModel.premiumMultiplier ?? existingModel.premiumMultiplier,
1048
1206
  } as Model<Api>);
1049
1207
  } else {
@@ -1120,11 +1278,20 @@ export class ModelRegistry {
1120
1278
  }
1121
1279
 
1122
1280
  #normalizeDiscoverableModels(providerConfig: DiscoveryProviderConfig, models: Model<Api>[]): Model<Api>[] {
1123
- if (providerConfig.provider !== "ollama" || providerConfig.api !== "openai-responses") {
1124
- return models;
1125
- }
1126
-
1127
- return models.map(model => (model.api === "openai-completions" ? { ...model, api: "openai-responses" } : model));
1281
+ return models.map(model => {
1282
+ const normalized =
1283
+ providerConfig.provider === "ollama" &&
1284
+ providerConfig.api === "openai-responses" &&
1285
+ model.api === "openai-completions"
1286
+ ? ({ ...model, api: "openai-responses" } as Model<Api>)
1287
+ : model;
1288
+ return {
1289
+ ...normalized,
1290
+ requestTransform: providerConfig.requestTransform
1291
+ ? mergeRequestTransform(undefined, providerConfig.requestTransform)
1292
+ : undefined,
1293
+ };
1294
+ });
1128
1295
  }
1129
1296
 
1130
1297
  #addImplicitDiscoverableProviders(configuredProviders: Set<string>): void {
@@ -1208,6 +1375,7 @@ export class ModelRegistry {
1208
1375
  providerConfig.authHeader !== undefined ||
1209
1376
  providerConfig.compat ||
1210
1377
  providerConfig.disableStrictTools ||
1378
+ providerConfig.requestTransform ||
1211
1379
  providerConfig.transport
1212
1380
  ) {
1213
1381
  const disableStrictCompat = providerConfig.disableStrictTools ? { disableStrictTools: true } : undefined;
@@ -1218,6 +1386,7 @@ export class ModelRegistry {
1218
1386
  authHeader: providerConfig.authHeader,
1219
1387
  compat: mergeCompat(providerConfig.compat, disableStrictCompat),
1220
1388
  transport: providerConfig.transport,
1389
+ requestTransform: providerConfig.requestTransform,
1221
1390
  });
1222
1391
  }
1223
1392
 
@@ -1233,6 +1402,7 @@ export class ModelRegistry {
1233
1402
  baseUrl: providerConfig.baseUrl ?? resolveProviderBaseUrlFromEnv(providerName),
1234
1403
  headers: providerConfig.headers,
1235
1404
  compat: providerConfig.compat,
1405
+ requestTransform: providerConfig.requestTransform,
1236
1406
  discovery: providerConfig.discovery,
1237
1407
  optional: false,
1238
1408
  });
@@ -1270,10 +1440,83 @@ export class ModelRegistry {
1270
1440
  discoverableProviders,
1271
1441
  configuredProviders,
1272
1442
  equivalence: value.equivalence,
1443
+ modelBindings: value.modelBindings,
1273
1444
  found: true,
1274
1445
  };
1275
1446
  }
1276
1447
 
1448
+ applyConfiguredModelBindings(targetSettings: Settings): void {
1449
+ this.#modelBindingsTargetSettings = targetSettings;
1450
+ this.#applyConfiguredModelBindingsToTarget();
1451
+ }
1452
+
1453
+ #applyConfiguredModelBindingsToTarget(): void {
1454
+ const targetSettings = this.#modelBindingsTargetSettings;
1455
+ if (!targetSettings) return;
1456
+ const bindings = this.#configuredModelBindings;
1457
+ const nextModelRoles = { ...targetSettings.get("modelRoles") };
1458
+ const configuredModelRoles = bindings?.modelRoles ?? {};
1459
+ const configuredModelRoleKeys = new Set(Object.keys(configuredModelRoles));
1460
+ for (const role of this.#appliedModelBindingRoles) {
1461
+ if (configuredModelRoleKeys.has(role)) continue;
1462
+ const lastApplied = this.#lastAppliedModelBindingRoles.get(role);
1463
+ if (lastApplied !== undefined && nextModelRoles[role] === lastApplied) {
1464
+ const baseline = this.#modelBindingRoleBaselines.get(role);
1465
+ if (baseline === undefined) {
1466
+ delete nextModelRoles[role];
1467
+ } else {
1468
+ nextModelRoles[role] = baseline;
1469
+ }
1470
+ }
1471
+ this.#modelBindingRoleBaselines.delete(role);
1472
+ this.#lastAppliedModelBindingRoles.delete(role);
1473
+ }
1474
+ for (const [role, modelId] of Object.entries(configuredModelRoles)) {
1475
+ if (!modelId) continue;
1476
+ const previousApplied = this.#lastAppliedModelBindingRoles.get(role);
1477
+ if (!this.#modelBindingRoleBaselines.has(role)) {
1478
+ this.#modelBindingRoleBaselines.set(role, nextModelRoles[role]);
1479
+ }
1480
+ if (previousApplied === undefined || nextModelRoles[role] === previousApplied) {
1481
+ nextModelRoles[role] = modelId;
1482
+ this.#lastAppliedModelBindingRoles.set(role, modelId);
1483
+ }
1484
+ }
1485
+ targetSettings.override("modelRoles", nextModelRoles);
1486
+ this.#appliedModelBindingRoles = new Set(Object.keys(configuredModelRoles));
1487
+
1488
+ const nextAgentModelOverrides = { ...targetSettings.get("task.agentModelOverrides") };
1489
+ const configuredAgentModelOverrides = bindings?.agentModelOverrides ?? {};
1490
+ const configuredAgentModelOverrideKeys = new Set(Object.keys(configuredAgentModelOverrides));
1491
+ for (const agentName of this.#appliedAgentModelBindingOverrides) {
1492
+ if (configuredAgentModelOverrideKeys.has(agentName)) continue;
1493
+ const lastApplied = this.#lastAppliedAgentModelBindingOverrides.get(agentName);
1494
+ if (lastApplied !== undefined && nextAgentModelOverrides[agentName] === lastApplied) {
1495
+ const baseline = this.#agentModelBindingBaselines.get(agentName);
1496
+ if (baseline === undefined) {
1497
+ delete nextAgentModelOverrides[agentName];
1498
+ } else {
1499
+ nextAgentModelOverrides[agentName] = baseline;
1500
+ }
1501
+ }
1502
+ this.#agentModelBindingBaselines.delete(agentName);
1503
+ this.#lastAppliedAgentModelBindingOverrides.delete(agentName);
1504
+ }
1505
+ for (const [agentName, modelId] of Object.entries(configuredAgentModelOverrides)) {
1506
+ if (!modelId) continue;
1507
+ const previousApplied = this.#lastAppliedAgentModelBindingOverrides.get(agentName);
1508
+ if (!this.#agentModelBindingBaselines.has(agentName)) {
1509
+ this.#agentModelBindingBaselines.set(agentName, nextAgentModelOverrides[agentName]);
1510
+ }
1511
+ if (previousApplied === undefined || nextAgentModelOverrides[agentName] === previousApplied) {
1512
+ nextAgentModelOverrides[agentName] = modelId;
1513
+ this.#lastAppliedAgentModelBindingOverrides.set(agentName, modelId);
1514
+ }
1515
+ }
1516
+ targetSettings.override("task.agentModelOverrides", nextAgentModelOverrides);
1517
+ this.#appliedAgentModelBindingOverrides = new Set(Object.keys(configuredAgentModelOverrides));
1518
+ }
1519
+
1277
1520
  async #refreshRuntimeDiscoveries(
1278
1521
  strategy: ModelRefreshStrategy,
1279
1522
  providerFilter?: ReadonlySet<string>,
@@ -1838,11 +2081,15 @@ export class ModelRegistry {
1838
2081
  headers: override.headers ? { ...(baseOverride?.headers ?? {}), ...override.headers } : baseOverride?.headers,
1839
2082
  compat: override.compat ? mergeCompat(baseOverride?.compat, override.compat) : baseOverride?.compat,
1840
2083
  transport: override.transport ?? baseOverride?.transport,
2084
+ requestTransform: mergeRequestTransform(baseOverride?.requestTransform, override.requestTransform),
1841
2085
  };
1842
2086
  }
1843
2087
  #applyProviderTransportOverride<T extends { baseUrl?: string; headers?: Record<string, string> }>(
1844
2088
  entry: T,
1845
- override: Pick<ProviderOverride, "baseUrl" | "headers" | "authHeader" | "apiKey" | "transport">,
2089
+ override: Pick<
2090
+ ProviderOverride,
2091
+ "baseUrl" | "headers" | "authHeader" | "apiKey" | "transport" | "requestTransform"
2092
+ >,
1846
2093
  ): T {
1847
2094
  const headers = mergeAuthHeader(
1848
2095
  override.headers ? { ...entry.headers, ...override.headers } : entry.headers,
@@ -1856,6 +2103,10 @@ export class ModelRegistry {
1856
2103
  // Preserve the model's existing transport when the override omits one;
1857
2104
  // providers without a `transport` field keep the default per-API dispatch.
1858
2105
  ...(override.transport !== undefined ? { transport: override.transport } : {}),
2106
+ requestTransform: mergeRequestTransform(
2107
+ (entry as { requestTransform?: ModelRequestTransform }).requestTransform,
2108
+ override.requestTransform,
2109
+ ),
1859
2110
  };
1860
2111
  }
1861
2112
  #applyRuntimeProviderOverrides(models: Model<Api>[]): Model<Api>[] {
@@ -1942,6 +2193,7 @@ export class ModelRegistry {
1942
2193
  providerConfig.apiKeyEnv ? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv) : providerConfig.apiKey,
1943
2194
  providerConfig.authHeader,
1944
2195
  providerCompat,
2196
+ providerConfig.requestTransform,
1945
2197
  (providerConfig.auth as ProviderAuthMode | undefined) ?? undefined,
1946
2198
  modelDef as CustomModelDefinitionLike,
1947
2199
  );
@@ -2239,6 +2491,7 @@ export class ModelRegistry {
2239
2491
  apiKey: config.apiKey,
2240
2492
  api: config.api,
2241
2493
  oauthConfigured: Boolean(config.oauth),
2494
+ requestTransform: config.requestTransform,
2242
2495
  models: (config.models ?? []) as ProviderValidationModel[],
2243
2496
  },
2244
2497
  "runtime-register",
@@ -2302,6 +2555,7 @@ export class ModelRegistry {
2302
2555
  config.apiKey,
2303
2556
  config.authHeader,
2304
2557
  config.compat,
2558
+ config.requestTransform,
2305
2559
  undefined,
2306
2560
  modelDef as CustomModelDefinitionLike,
2307
2561
  );
@@ -2346,6 +2600,7 @@ export class ModelRegistry {
2346
2600
  config.headers ||
2347
2601
  config.apiKey ||
2348
2602
  config.authHeader !== undefined ||
2603
+ config.requestTransform !== undefined ||
2349
2604
  config.transport !== undefined
2350
2605
  ) {
2351
2606
  const transportOverride = {
@@ -2353,6 +2608,7 @@ export class ModelRegistry {
2353
2608
  headers: config.headers,
2354
2609
  apiKey: config.apiKey,
2355
2610
  authHeader: config.authHeader,
2611
+ requestTransform: config.requestTransform,
2356
2612
  transport: config.transport,
2357
2613
  };
2358
2614
  const nextRuntimeOverride = this.#mergeProviderOverride(
@@ -2400,6 +2656,7 @@ export interface ProviderConfigInput {
2400
2656
  streamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
2401
2657
  headers?: Record<string, string>;
2402
2658
  compat?: Model<Api>["compat"];
2659
+ requestTransform?: ModelRequestTransform;
2403
2660
  authHeader?: boolean;
2404
2661
  /** Streaming transport override — see {@link Model.transport}. */
2405
2662
  transport?: Model<Api>["transport"];
@@ -2423,6 +2680,8 @@ export interface ProviderConfigInput {
2423
2680
  maxTokens: number;
2424
2681
  headers?: Record<string, string>;
2425
2682
  compat?: Model<Api>["compat"];
2683
+ requestTransform?: ModelRequestTransform;
2684
+ wireModelId?: string;
2426
2685
  contextPromotionTarget?: string;
2427
2686
  premiumMultiplier?: number;
2428
2687
  }>;