@gajae-code/coding-agent 0.1.3 → 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.
Files changed (50) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/types/cli/skills-cli.d.ts +9 -0
  3. package/dist/types/commands/gjc-runtime-bridge.d.ts +24 -0
  4. package/dist/types/commands/skills.d.ts +26 -0
  5. package/dist/types/config/model-registry.d.ts +31 -2
  6. package/dist/types/config/models-config-schema.d.ts +39 -0
  7. package/dist/types/gjc-runtime/launch-tmux.d.ts +23 -0
  8. package/dist/types/gjc-runtime/team-runtime.d.ts +35 -1
  9. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +15 -10
  10. package/dist/types/hooks/skill-state.d.ts +4 -1
  11. package/dist/types/modes/components/model-selector.d.ts +21 -1
  12. package/dist/types/skill-state/active-state.d.ts +19 -0
  13. package/dist/types/skill-state/workflow-hud.d.ts +62 -0
  14. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  15. package/package.json +7 -7
  16. package/src/cli/args.ts +14 -0
  17. package/src/cli/skills-cli.ts +88 -0
  18. package/src/cli.ts +1 -0
  19. package/src/commands/deep-interview.ts +21 -2
  20. package/src/commands/gjc-runtime-bridge.ts +161 -15
  21. package/src/commands/ralplan.ts +21 -2
  22. package/src/commands/skills.ts +48 -0
  23. package/src/commands/team.ts +54 -3
  24. package/src/commands/ultragoal.ts +21 -1
  25. package/src/commit/agentic/index.ts +1 -0
  26. package/src/commit/pipeline.ts +1 -0
  27. package/src/config/model-registry.ts +259 -8
  28. package/src/config/models-config-schema.ts +18 -0
  29. package/src/defaults/gjc/skills/deep-interview/SKILL.md +6 -6
  30. package/src/defaults/gjc/skills/ralplan/SKILL.md +5 -9
  31. package/src/defaults/gjc/skills/team/SKILL.md +4 -4
  32. package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -8
  33. package/src/gjc-runtime/launch-tmux.ts +73 -2
  34. package/src/gjc-runtime/team-runtime.ts +285 -34
  35. package/src/gjc-runtime/ultragoal-guard.ts +43 -1
  36. package/src/gjc-runtime/ultragoal-runtime.ts +307 -187
  37. package/src/hooks/skill-state.ts +4 -1
  38. package/src/internal-urls/docs-index.generated.ts +1 -1
  39. package/src/main.ts +10 -1
  40. package/src/modes/components/model-selector.ts +109 -28
  41. package/src/modes/components/skill-hud/render.ts +35 -8
  42. package/src/modes/controllers/selector-controller.ts +42 -2
  43. package/src/prompts/system/system-prompt.md +5 -4
  44. package/src/sdk.ts +1 -0
  45. package/src/session/agent-session.ts +6 -0
  46. package/src/setup/provider-onboarding.ts +2 -0
  47. package/src/skill-state/active-state.ts +104 -4
  48. package/src/skill-state/workflow-hud.ts +160 -0
  49. package/src/slash-commands/acp-builtins.ts +11 -2
  50. 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
- 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));
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<ProviderOverride, "baseUrl" | "headers" | "authHeader" | "apiKey" | "transport">,
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
 
@@ -60,7 +60,7 @@ Inspired by the [Ouroboros project](https://github.com/Q00/ouroboros) which demo
60
60
 
61
61
  ## Native Plugin Invocation Guard (Issue #3030)
62
62
 
63
- If this raw bundled skill is loaded by GJC's native skill loader through `/skill:deep-interview` or `gjc deep-interview`, do not treat that path as permission to skip rendered GJC setup. The user-facing invocation is `/skill:deep-interview`; do not recommend or advertise deprecated aliases as the deep-interview entrypoint. Regardless of invocation path, Phase 0 below remains blocking and must resolve `gjc.deepInterview.ambiguityThreshold` from settings before any announcement, state write, question, or ambiguity score.
63
+ If this raw bundled skill is loaded by GJC's native skill loader through `/skill:deep-interview`, do not treat that path as permission to skip rendered GJC setup. The user-facing invocation is `/skill:deep-interview`; do not recommend or advertise CLI bridge commands as the deep-interview entrypoint. Regardless of invocation path, Phase 0 below remains blocking and must resolve `gjc.deepInterview.ambiguityThreshold` from settings before any announcement, state write, question, or ambiguity score.
64
64
 
65
65
  ## Phase 0: Resolve Ambiguity Threshold (blocking prerequisite)
66
66
 
@@ -493,26 +493,26 @@ After the spec is written, mark it `pending approval` and present execution opti
493
493
 
494
494
  1. **Refine with ralplan consensus (Recommended)**
495
495
  - Description: "Consensus-refine this spec with Planner/Architect/Critic, then stop for explicit execution approval. Maximum quality."
496
- - Action: Only after the user selects this option, invoke `/skill:ralplan` or `gjc ralplan --consensus --direct` with the spec file path as context. The `--direct` flag skips the ralplan skill's interview phase (the deep interview already gathered requirements), while `--consensus` triggers the Planner/Architect/Critic loop. When consensus completes and produces a plan in `.gjc/plans/`, stop with that plan marked `pending approval`; do not automatically invoke execution or any other execution skill.
496
+ - Action: Only after the user selects this option, invoke `/skill:ralplan --consensus --direct` with the spec file path as context. The `--direct` flag skips the ralplan skill's interview phase (the deep interview already gathered requirements), while `--consensus` triggers the Planner/Architect/Critic loop. When consensus completes and produces a plan in `.gjc/plans/`, stop with that plan marked `pending approval`; do not automatically invoke execution or any other execution skill.
497
497
  - Pipeline: `deep-interview spec → explicit approval to refine → ralplan --consensus --direct → pending approval → separate execution approval`
498
498
 
499
499
  2. **Execute with team**
500
500
  - Description: "Full autonomous pipeline — planning, parallel implementation, QA, validation. Faster but without consensus refinement."
501
- - Action: Invoke `/skill:team` or `gjc team` with the spec file path as context only after the user explicitly selects this execution option. The spec replaces team planning input.
501
+ - Action: Invoke `/skill:team` with the spec file path as context only after the user explicitly selects this execution option. The spec replaces team planning input.
502
502
 
503
503
  3. **Execute with team**
504
504
  - Description: "Persistence loop with architect verification — keeps working until all acceptance criteria pass"
505
- - Action: Invoke `/skill:team` or `gjc team` with the spec file path as the task definition.
505
+ - Action: Invoke `/skill:team` with the spec file path as the task definition.
506
506
 
507
507
  4. **Execute with team**
508
508
  - Description: "N coordinated parallel agents — fastest execution for large specs"
509
- - Action: Invoke `/skill:team` or `gjc team` with the spec file path as the shared plan.
509
+ - Action: Invoke `/skill:team` with the spec file path as the shared plan.
510
510
 
511
511
  5. **Refine further**
512
512
  - Description: "Continue interviewing to improve clarity (current: {score}%)"
513
513
  - Action: Return to Phase 2 interview loop.
514
514
 
515
- **IMPORTANT:** On explicit execution selection, **MUST** use the chosen public GJC workflow entrypoint (`/skill:ralplan`, `/skill:team`, `gjc ralplan`, or `gjc team`). Do NOT implement directly. The deep-interview agent is a requirements agent, not an execution agent. If oversized initial context was summarized, pass the spec and prompt-safe summary forward, not the raw oversized source material. Without explicit execution selection, stop with the spec marked `pending approval`.
515
+ **IMPORTANT:** On explicit execution selection, **MUST** use the chosen bundled GJC workflow skill entrypoint (`/skill:ralplan` or `/skill:team`) inside the agent session. Do NOT use `gjc ralplan` unless a private runtime bridge is explicitly configured; that CLI command is a bridge-only compatibility endpoint. `gjc team` is a native tmux runtime command and may be used only when the Team workflow explicitly requires the CLI runtime. Do NOT implement directly. The deep-interview agent is a requirements agent, not an execution agent. If oversized initial context was summarized, pass the spec and prompt-safe summary forward, not the raw oversized source material. Without explicit execution selection, stop with the spec marked `pending approval`.
516
516
 
517
517
  ### Approval-Gated Refinement Path (Recommended)
518
518