Browse Source

Merge branch 'omos/pr-699-model-flag'

Alvin Unreal 1 month ago
parent
commit
d314331d44

+ 1 - 0
docs/configuration.md

@@ -104,6 +104,7 @@ All config files support **JSONC** (JSON with Comments):
 | Option | Type | Default | Description |
 |--------|------|---------|-------------|
 | `preset` | string | - | Active preset name (e.g. `"openai"`, `"best"`) |
+| `stripOrchestratorModel` | boolean | `false` | Preserve a runtime `/model` selection for the orchestrator after subagent dispatch by omitting its configured model from the SDK config. A selected preset's explicit `orchestrator.model` is retained. Without a runtime selection, this opt-in delegates the initial orchestrator choice to OpenCode's session default. |
 
 ### Runtime Preset Switching
 

+ 4 - 0
oh-my-opencode-slim.schema.json

@@ -12,6 +12,10 @@
       "description": "Use the compact TUI sidebar layout. Defaults to true; set false to use the expanded layout.",
       "type": "boolean"
     },
+    "stripOrchestratorModel": {
+      "description": "When true, omit orchestrator.model and orchestrator.variant from the SDK config so OpenCode uses the session model selected with /model after subagent dispatch. An explicitly selected preset that sets orchestrator.model is preserved. Defaults to false.",
+      "type": "boolean"
+    },
     "autoUpdate": {
       "description": "Disable automatic installation of plugin updates when false. Defaults to true.",
       "type": "boolean"

+ 6 - 0
src/config/schema.ts

@@ -310,6 +310,12 @@ export const PluginConfigSchema = z
       .describe(
         'Use the compact TUI sidebar layout. Defaults to true; set false to use the expanded layout.',
       ),
+    stripOrchestratorModel: z
+      .boolean()
+      .optional()
+      .describe(
+        'When true, omit orchestrator.model and orchestrator.variant from the SDK config so OpenCode uses the session model selected with /model after subagent dispatch. An explicitly selected preset that sets orchestrator.model is preserved. Defaults to false.',
+      ),
     autoUpdate: z
       .boolean()
       .optional()

+ 128 - 0
src/config/strip-orchestrator-model.test.ts

@@ -0,0 +1,128 @@
+import { describe, expect, test } from 'bun:test';
+import { applyOrchestratorModelConfig } from './strip-orchestrator-model';
+
+describe('applyOrchestratorModelConfig', () => {
+  test('preserves a runtime /model selection by removing configured model and variant', () => {
+    const agents = {
+      orchestrator: { model: 'openai/gpt-5', variant: 'high' },
+    };
+
+    applyOrchestratorModelConfig({
+      agents,
+      enabled: true,
+      presets: undefined,
+      configPreset: undefined,
+      runtimePreset: null,
+    });
+
+    expect(agents.orchestrator).toEqual({});
+  });
+
+  test('retains model and variant when disabled or a selected preset sets a model', () => {
+    const disabled = {
+      orchestrator: { model: 'openai/gpt-5', variant: 'high' },
+    };
+    const presetOverride = {
+      orchestrator: { model: 'openai/gpt-5', variant: 'high' },
+    };
+
+    applyOrchestratorModelConfig({
+      agents: disabled,
+      enabled: false,
+      presets: undefined,
+      configPreset: undefined,
+      runtimePreset: null,
+    });
+    applyOrchestratorModelConfig({
+      agents: presetOverride,
+      enabled: true,
+      presets: {
+        file: { orchestrator: { model: 'anthropic/claude-sonnet-4' } },
+      },
+      configPreset: 'file',
+      runtimePreset: null,
+    });
+
+    expect(disabled.orchestrator).toEqual({
+      model: 'openai/gpt-5',
+      variant: 'high',
+    });
+    expect(presetOverride.orchestrator).toEqual({
+      model: 'openai/gpt-5',
+      variant: 'high',
+    });
+  });
+
+  test('uses the runtime preset before the file preset after /preset changes', () => {
+    const agents = {
+      orchestrator: { model: 'openai/gpt-5', variant: 'high' },
+    };
+
+    applyOrchestratorModelConfig({
+      agents,
+      enabled: true,
+      presets: {
+        file: { orchestrator: { model: 'anthropic/claude-sonnet-4' } },
+        runtime: { explorer: { model: 'openai/gpt-5-mini' } },
+      },
+      configPreset: 'file',
+      runtimePreset: 'runtime',
+    });
+
+    expect(agents.orchestrator).toEqual({});
+  });
+
+  test('skips stripping when the active runtime preset sets the orchestrator model', () => {
+    const agents = {
+      orchestrator: { model: 'openai/gpt-5', variant: 'high' },
+    };
+
+    applyOrchestratorModelConfig({
+      agents,
+      enabled: true,
+      presets: {
+        file: { explorer: { model: 'openai/gpt-5-mini' } },
+        runtime: { orchestrator: { model: 'anthropic/claude-sonnet-4' } },
+      },
+      configPreset: 'file',
+      runtimePreset: 'runtime',
+    });
+
+    expect(agents.orchestrator).toEqual({
+      model: 'openai/gpt-5',
+      variant: 'high',
+    });
+  });
+
+  test('leaves a primitive orchestrator config unchanged', () => {
+    const agents: Record<string, unknown> = { orchestrator: 'invalid' };
+
+    applyOrchestratorModelConfig({
+      agents,
+      enabled: true,
+      presets: undefined,
+      configPreset: undefined,
+      runtimePreset: null,
+    });
+
+    expect(agents.orchestrator).toBe('invalid');
+  });
+
+  test('allows TUI state to capture the configured model and variant before stripping', () => {
+    const agents = {
+      orchestrator: { model: 'openai/gpt-5', variant: 'high' },
+    };
+    const tuiState = { ...agents.orchestrator };
+
+    applyOrchestratorModelConfig({
+      agents,
+      enabled: true,
+      presets: undefined,
+      configPreset: undefined,
+      runtimePreset: null,
+    });
+
+    expect(tuiState).toEqual({ model: 'openai/gpt-5', variant: 'high' });
+    expect(agents.orchestrator).toEqual({});
+  });
+});

+ 34 - 0
src/config/strip-orchestrator-model.ts

@@ -0,0 +1,34 @@
+import type { PluginConfig, Preset } from './schema';
+
+function isRecord(value: unknown): value is Record<string, unknown> {
+  return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+export function stripOrchestratorModel(
+  agents: Record<string, unknown>,
+  enabled: boolean | undefined,
+  preset: Preset | undefined,
+): void {
+  if (enabled !== true || preset?.orchestrator?.model !== undefined) return;
+
+  const orchestrator = agents.orchestrator;
+  if (!isRecord(orchestrator)) return;
+
+  delete orchestrator.model;
+  delete orchestrator.variant;
+}
+
+export function applyOrchestratorModelConfig(input: {
+  agents: Record<string, unknown>;
+  enabled: boolean | undefined;
+  presets: PluginConfig['presets'];
+  configPreset: string | undefined;
+  runtimePreset: string | null;
+}): void {
+  const presetName = input.runtimePreset ?? input.configPreset;
+  stripOrchestratorModel(
+    input.agents,
+    input.enabled,
+    presetName ? input.presets?.[presetName] : undefined,
+  );
+}

+ 12 - 0
src/index.ts

@@ -21,6 +21,7 @@ import {
   getPreviousRuntimePreset,
   setActiveRuntimePreset,
 } from './config/runtime-preset';
+import { applyOrchestratorModelConfig } from './config/strip-orchestrator-model';
 import { CouncilManager } from './council';
 import {
   createApplyPatchHook,
@@ -739,6 +740,9 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         }
       }
 
+      // Capture the resolved model state before optionally removing the
+      // orchestrator model from the SDK config, so the TUI keeps showing the
+      // configured model rather than a fallback or "default".
       const tuiAgentModels: Record<string, string> = {};
       const tuiAgentVariants: Record<string, string> = {};
       for (const agentDef of agentDefs) {
@@ -772,6 +776,14 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         agentVariants: tuiAgentVariants,
       });
 
+      applyOrchestratorModelConfig({
+        agents: configAgent,
+        enabled: config.stripOrchestratorModel,
+        presets: config.presets,
+        configPreset: config.preset,
+        runtimePreset: runtimePresetName,
+      });
+
       // Merge MCP configs
       const configMcp = opencodeConfig.mcp as
         | Record<string, unknown>