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

Merge pull request #571 from alvinunreal/bugfix/installer-model-array-detection

fix: installer model provider detection
Alvin 2 месяцев назад
Родитель
Сommit
6f39d0f7a5

+ 25 - 0
src/cli/config-io.test.ts

@@ -580,6 +580,31 @@ describe('config-io', () => {
     expect(detected.hasTmux).toBe(true);
   });
 
+  test('detectCurrentConfig detects provider models in arrays', () => {
+    const configPath = join(tmpDir, 'opencode', 'opencode.json');
+    const litePath = join(tmpDir, 'opencode', 'oh-my-opencode-slim.json');
+    paths.ensureConfigDir();
+
+    writeFileSync(configPath, JSON.stringify({ plugin: ['oh-my-opencode-slim'] }));
+    writeFileSync(
+      litePath,
+      JSON.stringify({
+        preset: 'dev',
+        presets: {
+          dev: {
+            orchestrator: {
+              model: ['openai/gpt-5.4-mini', { id: 'anthropic/claude-opus-4-6' }],
+            },
+          },
+        },
+      }),
+    );
+
+    const detected = detectCurrentConfig();
+    expect(detected.hasOpenAI).toBe(true);
+    expect(detected.hasAnthropic).toBe(true);
+  });
+
   test('detectCurrentConfig treats local repo path entries as installed', () => {
     const configPath = join(tmpDir, 'opencode', 'opencode.json');
     const packageRoot = join(tmpDir, 'repo');

+ 24 - 11
src/cli/config-io.ts

@@ -34,6 +34,19 @@ function isString(value: unknown): value is string {
   return typeof value === 'string';
 }
 
+function getModelIds(model: unknown): string[] {
+  if (isString(model)) return [model];
+  if (!Array.isArray(model)) return [];
+
+  return model.flatMap((entry) => {
+    if (isString(entry)) return [entry];
+    if (entry && typeof entry === 'object' && isString(entry.id)) {
+      return [entry.id];
+    }
+    return [];
+  });
+}
+
 function getPlugins(config: OpenCodeConfig): unknown[] {
   return Array.isArray(config.plugin) ? config.plugin : [];
 }
@@ -669,22 +682,22 @@ export function detectCurrentConfig(): DetectedConfig {
     const presetName = configObj.preset as string;
     const presets = configObj.presets as Record<string, unknown>;
     const agents = presets?.[presetName] as
-      | Record<string, { model?: string }>
+      | Record<string, { model?: unknown }>
       | undefined;
 
-    if (agents) {
+    if (agents && typeof agents === 'object') {
       const models = Object.values(agents)
-        .map((a) => a?.model)
-        .filter(Boolean);
-      result.hasOpenAI = models.some((m) => m?.startsWith('openai/'));
-      result.hasAnthropic = models.some((m) => m?.startsWith('anthropic/'));
-      result.hasCopilot = models.some((m) => m?.startsWith('github-copilot/'));
-      result.hasZaiPlan = models.some((m) => m?.startsWith('zai-coding-plan/'));
-      result.hasOpencodeZen = models.some((m) => m?.startsWith('opencode/'));
-      if (models.some((m) => m?.startsWith('google/'))) {
+        .filter((a) => a && typeof a === 'object')
+        .flatMap((a) => getModelIds(a.model));
+      result.hasOpenAI ||= models.some((m) => m.startsWith('openai/'));
+      result.hasAnthropic ||= models.some((m) => m.startsWith('anthropic/'));
+      result.hasCopilot ||= models.some((m) => m.startsWith('github-copilot/'));
+      result.hasZaiPlan ||= models.some((m) => m.startsWith('zai-coding-plan/'));
+      result.hasOpencodeZen ||= models.some((m) => m.startsWith('opencode/'));
+      if (models.some((m) => m.startsWith('google/'))) {
         result.hasAntigravity = true;
       }
-      if (models.some((m) => m?.startsWith('chutes/'))) {
+      if (models.some((m) => m.startsWith('chutes/'))) {
         result.hasChutes = true;
       }
     }

+ 38 - 1
src/hooks/task-session-manager/index.test.ts

@@ -1169,7 +1169,7 @@ describe('task-session-manager hook', () => {
     });
 
     const resume = {
-      args: { subagent_type: 'not-an-agent', task_id: 'exp-1' },
+      args: { subagent_type: 123, task_id: 'exp-1' },
     };
     await hook['tool.execute.before'](
       { tool: 'task', sessionID: 'parent-1', callID: 'resume' },
@@ -1179,6 +1179,43 @@ describe('task-session-manager hook', () => {
     expect(resume.args.task_id).toBeUndefined();
   });
 
+  test('custom subagent raw session task_id is preserved', async () => {
+    const { hook } = createHook();
+    const resume = {
+      args: { subagent_type: 'repro-helper', task_id: 'ses_custom123' },
+    };
+
+    await hook['tool.execute.before'](
+      { tool: 'task', sessionID: 'parent-1', callID: 'resume' },
+      resume,
+    );
+
+    expect(resume.args.task_id).toBe('ses_custom123');
+  });
+
+  test('custom subagent aliases resolve for the same custom agent', async () => {
+    const board = new BackgroundJobBoard();
+    const { hook } = createHook({ backgroundJobBoard: board });
+    board.registerLaunch({
+      taskID: 'child-1',
+      parentSessionID: 'parent-1',
+      agent: 'repro-helper',
+      description: 'ask secret letter',
+    });
+    board.updateStatus({ taskID: 'child-1', state: 'completed' });
+    board.markReconciled('child-1');
+
+    const resume = {
+      args: { subagent_type: 'repro-helper', task_id: 'rep-1' },
+    };
+    await hook['tool.execute.before'](
+      { tool: 'task', sessionID: 'parent-1', callID: 'resume' },
+      resume,
+    );
+
+    expect(resume.args.task_id).toBe('child-1');
+  });
+
   test('wrong parent or wrong agent alias does not resolve', async () => {
     const board = new BackgroundJobBoard();
     const { hook } = createHook({ backgroundJobBoard: board });

+ 10 - 22
src/hooks/task-session-manager/index.ts

@@ -1,6 +1,5 @@
 import path from 'node:path';
 import type { PluginInput } from '@opencode-ai/plugin';
-import type { AgentName } from '../../config';
 import {
   BackgroundJobBoard,
   type BackgroundJobRecord,
@@ -25,23 +24,11 @@ interface TaskArgs {
 interface PendingTaskCall {
   callId: string;
   parentSessionId: string;
-  agentType: AgentName;
+  agentType: string;
   label: string;
   resumedTaskId?: string;
 }
 
-const AGENT_NAME_SET = new Set<AgentName>([
-  'orchestrator',
-  'oracle',
-  'designer',
-  'explorer',
-  'librarian',
-  'fixer',
-  'observer',
-  'council',
-  'councillor',
-]);
-
 const MAX_PENDING_TASK_CALLS = 100;
 
 interface PendingContextFile {
@@ -108,10 +95,6 @@ function createOccurrenceId(
   return `anon:${hash}`;
 }
 
-function isAgentName(value: unknown): value is AgentName {
-  return typeof value === 'string' && AGENT_NAME_SET.has(value as AgentName);
-}
-
 function extractPath(output: string): string | undefined {
   return /<path>([^<]+)<\/path>/.exec(output)?.[1];
 }
@@ -492,18 +475,23 @@ export function createTaskSessionManagerHook(
       if (!isObjectRecord(output.args)) return;
 
       const args = output.args as TaskArgs;
-      if (!isAgentName(args.subagent_type)) {
+      if (
+        typeof args.subagent_type !== 'string' ||
+        args.subagent_type.trim() === ''
+      ) {
         if (typeof args.task_id === 'string' && args.task_id.trim() !== '') {
           delete args.task_id;
         }
         return;
       }
 
+      const agentType = args.subagent_type.trim();
+
       const label = deriveTaskSessionLabel({
         description:
           typeof args.description === 'string' ? args.description : undefined,
         prompt: typeof args.prompt === 'string' ? args.prompt : undefined,
-        agentType: args.subagent_type,
+        agentType,
       });
 
       const pendingCall: PendingTaskCall = {
@@ -512,7 +500,7 @@ export function createTaskSessionManagerHook(
           sessionID: input.sessionID,
         }),
         parentSessionId: input.sessionID,
-        agentType: args.subagent_type,
+        agentType,
         label,
       };
       rememberPendingCall(pendingCall);
@@ -525,7 +513,7 @@ export function createTaskSessionManagerHook(
       const remembered = backgroundJobBoard.resolveReusable(
         input.sessionID,
         requested,
-        args.subagent_type,
+        agentType,
       );
 
       if (!remembered) {