Просмотр исходного кода

fix: disable fallback chain when model switched via /model

Inline the check in the config hook. When a user picks a model via /model
and it differs from the configured chain primary, disable fallback for
that agent.
Michael Henke 1 месяц назад
Родитель
Сommit
8a9e0c3340
4 измененных файлов с 20 добавлено и 118 удалено
  1. 0 74
      src/hooks/foreground-fallback/index.test.ts
  2. 0 25
      src/hooks/foreground-fallback/index.ts
  3. 0 1
      src/hooks/index.ts
  4. 20 18
      src/index.ts

+ 0 - 74
src/hooks/foreground-fallback/index.test.ts

@@ -1,7 +1,6 @@
 import { beforeEach, describe, expect, mock, test } from 'bun:test';
 import { SessionLifecycle } from '../session-lifecycle';
 import {
-  disableChainsForModelSwitches,
   ForegroundFallbackManager,
   isFailoverError,
   isRateLimitError,
@@ -1328,76 +1327,3 @@ describe('ForegroundFallbackManager disableChain', () => {
     expect(call[0].body.model.modelID).toBe('claude-haiku');
   });
 });
-
-// ---------------------------------------------------------------------------
-// disableChainsForModelSwitches
-// ---------------------------------------------------------------------------
-
-describe('disableChainsForModelSwitches', () => {
-  function makeMgr(chains: Record<string, string[]>) {
-    const { client } = createMockClient();
-    return new ForegroundFallbackManager(client, chains, true);
-  }
-
-  test('disables when model differs from chain[0]', () => {
-    const chains = { orchestrator: ['a', 'b'] };
-    const mgr = makeMgr(chains);
-    const configAgent = { orchestrator: { model: 'c' } };
-
-    const result = disableChainsForModelSwitches(mgr, chains, configAgent);
-
-    expect((mgr as any).chains.orchestrator).toEqual([]);
-    expect(result).toEqual(['orchestrator']);
-  });
-
-  test('does NOT disable when model equals chain[0]', () => {
-    const chains = { orchestrator: ['a', 'b'] };
-    const mgr = makeMgr(chains);
-    const configAgent = { orchestrator: { model: 'a' } };
-
-    const result = disableChainsForModelSwitches(mgr, chains, configAgent);
-
-    expect((mgr as any).chains.orchestrator).toEqual(['a', 'b']);
-    expect(result).toEqual([]);
-  });
-
-  test('does NOT disable when agent has no configAgent entry', () => {
-    const chains = { orchestrator: ['a', 'b'] };
-    const mgr = makeMgr(chains);
-
-    const result = disableChainsForModelSwitches(mgr, chains, {});
-
-    expect((mgr as any).chains.orchestrator).toEqual(['a', 'b']);
-    expect(result).toEqual([]);
-  });
-
-  test('skips empty chains', () => {
-    const chains = { orchestrator: [] };
-    const mgr = makeMgr(chains);
-    const configAgent = { orchestrator: { model: 'c' } };
-
-    expect(() =>
-      disableChainsForModelSwitches(mgr, chains, configAgent),
-    ).not.toThrow();
-    const result = disableChainsForModelSwitches(mgr, chains, configAgent);
-    expect(result).toEqual([]);
-  });
-
-  test('only disables the mismatched agent', () => {
-    const chains = {
-      orchestrator: ['a', 'b'],
-      explorer: ['x', 'y'],
-    };
-    const mgr = makeMgr(chains);
-    const configAgent = {
-      orchestrator: { model: 'c' }, // mismatched
-      explorer: { model: 'x' }, // matches chain[0]
-    };
-
-    const result = disableChainsForModelSwitches(mgr, chains, configAgent);
-
-    expect((mgr as any).chains.orchestrator).toEqual([]);
-    expect((mgr as any).chains.explorer).toEqual(['x', 'y']);
-    expect(result).toEqual(['orchestrator']);
-  });
-});

+ 0 - 25
src/hooks/foreground-fallback/index.ts

@@ -679,28 +679,3 @@ export class ForegroundFallbackManager {
     return all;
   }
 }
-
-/**
- * Disable fallback chains for agents whose resolved model differs from the
- * chain's primary model. Called from the config() hook after chains are
- * resolved, so a model chosen via /model (or a runtime preset) sticks
- * instead of silently falling back on rate-limit errors.
- * Returns the names of agents whose chain was disabled.
- */
-export function disableChainsForModelSwitches(
-  mgr: ForegroundFallbackManager,
-  runtimeChains: Record<string, string[]>,
-  configAgent: Record<string, unknown>,
-): string[] {
-  const disabled: string[] = [];
-  for (const agentName of Object.keys(runtimeChains)) {
-    const chain = runtimeChains[agentName];
-    if (!chain || chain.length === 0) continue;
-    const entry = configAgent[agentName] as Record<string, unknown> | undefined;
-    if (entry && typeof entry.model === 'string' && entry.model !== chain[0]) {
-      mgr.disableChain(agentName);
-      disabled.push(agentName);
-    }
-  }
-  return disabled;
-}

+ 0 - 1
src/hooks/index.ts

@@ -6,7 +6,6 @@ export { createDeepworkCommandHook } from './deepwork';
 export { createDelegateTaskRetryHook } from './delegate-task-retry/hook';
 export { createFilterAvailableSkillsHook } from './filter-available-skills';
 export {
-  disableChainsForModelSwitches,
   ForegroundFallbackManager,
   isFailoverError,
   isRateLimitError,

+ 20 - 18
src/index.ts

@@ -37,7 +37,6 @@ import {
   createPostFileToolNudgeHook,
   createReflectCommandHook,
   createTaskSessionManagerHook,
-  disableChainsForModelSwitches,
   ForegroundFallbackManager,
   SessionLifecycle,
 } from './hooks';
@@ -741,23 +740,26 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         }
       }
 
-      // Disable fallback chains for agents whose model was explicitly switched
-      // via /model (or runtime preset). After the switch, that agent's chain
-      // is emptied so ForegroundFallbackManager never silently falls
-      // back — rate-limit errors surface instead. The detection compares the
-      // resolved entry.model against the chain's primary model; any mismatch
-      // means the user (or preset) intentionally chose something different.
-      const disabledChains = disableChainsForModelSwitches(
-        foregroundFallback,
-        runtimeChains,
-        configAgent,
-      );
-      for (const agentName of disabledChains) {
-        log('[plugin] disabled fallback chain for model-switched agent', {
-          agent: agentName,
-          model: (configAgent[agentName] as Record<string, unknown>).model,
-          chainPrimary: runtimeChains[agentName]?.[0],
-        });
+      // Disable fallback for agents whose model was switched via /model.
+      // Empty chain → rate-limit errors surface instead of falling back.
+      for (const agentName of Object.keys(runtimeChains)) {
+        const chain = runtimeChains[agentName];
+        if (!chain || chain.length === 0) continue;
+        const entry = configAgent[agentName] as
+          | Record<string, unknown>
+          | undefined;
+        if (
+          entry &&
+          typeof entry.model === 'string' &&
+          entry.model !== chain[0]
+        ) {
+          foregroundFallback.disableChain(agentName);
+          log('[plugin] disabled fallback chain for model-switched agent', {
+            agent: agentName,
+            model: entry.model,
+            chainPrimary: chain[0],
+          });
+        }
       }
 
       // Capture the resolved model state before optionally removing the