diff --git a/docs/users/features/sub-agents.md b/docs/users/features/sub-agents.md
index 256034e3c..39c105383 100644
--- a/docs/users/features/sub-agents.md
+++ b/docs/users/features/sub-agents.md
@@ -98,6 +98,7 @@ Subagents are configured using Markdown files with YAML frontmatter. This format
---
name: agent-name
description: Brief description of when and how to use this agent
+model: inherit # Optional: inherit or model-id
tools:
- tool1
- tool2
@@ -106,9 +107,17 @@ tools:
System prompt content goes here.
Multiple paragraphs are supported.
-You can use ${variable} templating for dynamic content.
```
+#### Model Selection
+
+Use the optional `model` frontmatter field to control which model a subagent uses:
+
+- `inherit`: Use the same model as the main conversation
+- Omit the field: Same as `inherit`
+- `glm-5`: Use that model ID with the main conversation's auth type
+- `sonnet`, `opus`, `haiku`: Alias-style values are also accepted as model strings
+
#### Example Usage
```
@@ -117,12 +126,7 @@ name: project-documenter
description: Creates project documentation and README files
---
-You are a documentation specialist for the ${project_name} project.
-
-Your task: ${task_description}
-
-Working directory: ${current_directory}
-Generated on: ${timestamp}
+You are a documentation specialist.
Focus on creating clear, comprehensive documentation that helps both
new contributors and end users understand the project.
@@ -213,7 +217,7 @@ tools:
- web_search
---
-You are a technical documentation specialist for ${project_name}.
+You are a technical documentation specialist.
Your role is to create clear, comprehensive documentation that serves both
developers and end users. Focus on:
diff --git a/integration-tests/sdk-typescript/subagents.test.ts b/integration-tests/sdk-typescript/subagents.test.ts
index 487e0e7e7..d9fa037f7 100644
--- a/integration-tests/sdk-typescript/subagents.test.ts
+++ b/integration-tests/sdk-typescript/subagents.test.ts
@@ -131,16 +131,13 @@ describe('Subagents (E2E)', () => {
}
});
- it('should handle subagent with custom model config', async () => {
+ it('should handle subagent with custom model selector', async () => {
const customModelAgent: SubagentConfig = {
name: 'custom-model-agent',
description: 'Agent with custom model configuration',
systemPrompt: 'You are a helpful assistant.',
level: 'session',
- modelConfig: {
- temp: 0.7,
- top_p: 0.9,
- },
+ model: 'inherit',
};
const q = query({
diff --git a/packages/cli/src/ui/components/subagents/manage/AgentViewerStep.tsx b/packages/cli/src/ui/components/subagents/manage/AgentViewerStep.tsx
index ee2fd3664..a74ddc96b 100644
--- a/packages/cli/src/ui/components/subagents/manage/AgentViewerStep.tsx
+++ b/packages/cli/src/ui/components/subagents/manage/AgentViewerStep.tsx
@@ -40,6 +40,13 @@ export const AgentViewerStep = ({ selectedAgent }: AgentViewerStepProps) => {
{toolsDisplay}
+ {agent.model && (
+
+ {t('Model: ')}
+ {agent.model}
+
+ )}
+
{shouldShowColor(agent.color) && (
{t('Color: ')}
diff --git a/packages/core/src/agents/backends/InProcessBackend.ts b/packages/core/src/agents/backends/InProcessBackend.ts
index ba0fbf15d..6c23f8adf 100644
--- a/packages/core/src/agents/backends/InProcessBackend.ts
+++ b/packages/core/src/agents/backends/InProcessBackend.ts
@@ -19,7 +19,14 @@ import {
type ContentGeneratorConfig,
createContentGenerator,
} from '../../core/contentGenerator.js';
-import { AUTH_ENV_MAPPINGS } from '../../models/constants.js';
+import type { ToolRegistry } from '../../tools/tool-registry.js';
+import { WorkspaceContext } from '../../utils/workspaceContext.js';
+import { FileDiscoveryService } from '../../services/fileDiscoveryService.js';
+import {
+ AUTH_ENV_MAPPINGS,
+ MODEL_GENERATION_CONFIG_FIELDS,
+} from '../../models/constants.js';
+import type { ResolvedModelConfig } from '../../models/types.js';
import { AgentStatus, isTerminalStatus } from '../runtime/agent-types.js';
import { AgentCore } from '../runtime/agent-core.js';
import { AgentEventEmitter } from '../runtime/agent-events.js';
@@ -33,9 +40,6 @@ import type {
} from './types.js';
import { DISPLAY_MODE } from './types.js';
import type { AnsiOutput } from '../../utils/terminalSerializer.js';
-import { WorkspaceContext } from '../../utils/workspaceContext.js';
-import { FileDiscoveryService } from '../../services/fileDiscoveryService.js';
-import type { ToolRegistry } from '../../tools/tool-registry.js';
const debugLogger = createDebugLogger('IN_PROCESS_BACKEND');
@@ -332,15 +336,10 @@ export class InProcessBackend implements Backend {
* - `getWorkingDir()` / `getTargetDir()` → agent's worktree cwd
* - `getWorkspaceContext()` → WorkspaceContext rooted at agent's cwd
* - `getFileService()` → FileDiscoveryService rooted at agent's cwd
- * (so .qwenignore checks resolve against the agent's worktree)
* - `getToolRegistry()` → per-agent tool registry with core tools bound to
- * the agent Config (so tools resolve paths against the agent's worktree)
+ * the agent Config
* - `getContentGenerator()` / `getContentGeneratorConfig()` / `getAuthType()`
- * → per-agent ContentGenerator when `authOverrides` is provided, enabling
- * agents to target different model providers in the same Arena session
- *
- * Uses prototypal delegation so all other Config methods/properties resolve
- * against the original instance transparently.
+ * → per-agent ContentGenerator when `authOverrides` is provided
*/
async function createPerAgentConfig(
base: Config,
@@ -361,9 +360,6 @@ async function createPerAgentConfig(
const agentFileService = new FileDiscoveryService(cwd);
override.getFileService = () => agentFileService;
- // Build a per-agent tool registry: core tools are constructed with
- // the per-agent Config so they resolve paths against cwd. Discovered
- // (MCP/command) tools are copied from the parent registry as-is.
const agentRegistry: ToolRegistry = await override.createToolRegistry(
undefined,
{ skipDiscovery: true },
@@ -371,9 +367,6 @@ async function createPerAgentConfig(
agentRegistry.copyDiscoveredToolsFrom(base.getToolRegistry());
override.getToolRegistry = () => agentRegistry;
- // Build a per-agent ContentGenerator when auth overrides are provided.
- // This enables Arena agents to use different providers (OpenAI, Anthropic,
- // Gemini, etc.) than the parent process.
if (authOverrides?.authType) {
try {
const agentGeneratorConfig = buildAgentContentGeneratorConfig(
@@ -406,16 +399,6 @@ async function createPerAgentConfig(
return override as Config;
}
-/**
- * Build a ContentGeneratorConfig for a per-agent ContentGenerator.
- * Inherits operational settings (timeout, retries, proxy, sampling, etc.)
- * from the parent's config and overlays the agent-specific auth fields.
- *
- * For cross-provider agents the parent's API key / base URL are invalid,
- * so we resolve credentials from the provider-specific environment
- * variables (e.g. ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL). This mirrors
- * what a PTY subprocess does during its own initialization.
- */
function buildAgentContentGeneratorConfig(
base: Config,
modelId: string | undefined,
@@ -423,34 +406,101 @@ function buildAgentContentGeneratorConfig(
): ContentGeneratorConfig {
const parentConfig = base.getContentGeneratorConfig();
const sameProvider = authOverrides.authType === parentConfig.authType;
+ const modelsConfig = base.getModelsConfig();
+ const resolvedModel = modelId
+ ? modelsConfig.getResolvedModel(authOverrides.authType as AuthType, modelId)
+ : undefined;
- const resolvedApiKey = resolveCredentialField(
+ const nextConfig: ContentGeneratorConfig = {
+ ...parentConfig,
+ model: modelId ?? parentConfig.model,
+ authType: authOverrides.authType as AuthType,
+ };
+
+ // When switching providers, clear generation config fields so parent
+ // settings (samplingParams, reasoning, extra_body, etc.) don't leak.
+ if (!sameProvider) {
+ for (const field of MODEL_GENERATION_CONFIG_FIELDS) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (nextConfig as any)[field] = undefined;
+ }
+ }
+
+ if (resolvedModel) {
+ applyResolvedModelConfig(
+ nextConfig,
+ resolvedModel,
+ parentConfig,
+ authOverrides,
+ );
+ return nextConfig;
+ }
+
+ nextConfig.apiKey = resolveCredentialField(
authOverrides.apiKey,
sameProvider ? parentConfig.apiKey : undefined,
authOverrides.authType,
'apiKey',
);
+ nextConfig.baseUrl =
+ authOverrides.baseUrl ??
+ resolveCredentialField(
+ undefined,
+ sameProvider ? parentConfig.baseUrl : undefined,
+ authOverrides.authType,
+ 'baseUrl',
+ );
+ nextConfig.apiKeyEnvKey = sameProvider
+ ? parentConfig.apiKeyEnvKey
+ : undefined;
- const resolvedBaseUrl = resolveCredentialField(
- authOverrides.baseUrl,
- sameProvider ? parentConfig.baseUrl : undefined,
- authOverrides.authType,
- 'baseUrl',
- );
-
- return {
- ...parentConfig,
- model: modelId ?? parentConfig.model,
- authType: authOverrides.authType as AuthType,
- apiKey: resolvedApiKey,
- baseUrl: resolvedBaseUrl,
- };
+ return nextConfig;
+}
+
+function applyResolvedModelConfig(
+ targetConfig: ContentGeneratorConfig,
+ resolvedModel: ResolvedModelConfig,
+ parentConfig: ContentGeneratorConfig,
+ authOverrides: NonNullable,
+): void {
+ const sameProvider = authOverrides.authType === parentConfig.authType;
+ targetConfig.model = resolvedModel.id;
+ targetConfig.authType = resolvedModel.authType;
+ targetConfig.baseUrl =
+ authOverrides.baseUrl ??
+ resolvedModel.baseUrl ??
+ (sameProvider ? parentConfig.baseUrl : undefined);
+
+ if (resolvedModel.envKey) {
+ targetConfig.apiKey =
+ authOverrides.apiKey ??
+ process.env[resolvedModel.envKey] ??
+ (sameProvider ? parentConfig.apiKey : undefined);
+ targetConfig.apiKeyEnvKey = resolvedModel.envKey;
+ } else {
+ targetConfig.apiKey = resolveCredentialField(
+ authOverrides.apiKey,
+ sameProvider ? parentConfig.apiKey : undefined,
+ authOverrides.authType,
+ 'apiKey',
+ );
+ targetConfig.apiKeyEnvKey = sameProvider
+ ? parentConfig.apiKeyEnvKey
+ : undefined;
+ }
+
+ // Apply registry-defined generation config fields. Cross-provider
+ // clearing is already handled by buildAgentContentGeneratorConfig,
+ // so here we only overwrite when the registry provides a value.
+ for (const field of MODEL_GENERATION_CONFIG_FIELDS) {
+ const registryValue = resolvedModel.generationConfig[field];
+ if (registryValue !== undefined) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (targetConfig as any)[field] = registryValue;
+ }
+ }
}
-/**
- * Resolve a credential field (apiKey or baseUrl) with the following
- * priority: explicit override → same-provider parent value → env var.
- */
function resolveCredentialField(
explicitValue: string | undefined,
inheritedValue: string | undefined,
diff --git a/packages/core/src/extension/claude-converter.ts b/packages/core/src/extension/claude-converter.ts
index 9ef287827..b8da31672 100644
--- a/packages/core/src/extension/claude-converter.ts
+++ b/packages/core/src/extension/claude-converter.ts
@@ -182,14 +182,9 @@ export function convertClaudeAgentConfig(
qwenAgent['tools'] = claudeBuildInToolsTransform(claudeAgent.tools);
}
- // Convert model to modelConfig
+ // Preserve Claude's top-level model selector.
if (claudeAgent.model) {
- // Map Claude model names to Qwen model config
- // Claude uses: sonnet, opus, haiku, inherit
- // We preserve the model name for now, the actual mapping will be handled at runtime
- qwenAgent['modelConfig'] = {
- model: claudeAgent.model === 'inherit' ? undefined : claudeAgent.model,
- };
+ qwenAgent['model'] = claudeAgent.model;
}
// Preserve unsupported fields as-is for potential future compatibility
diff --git a/packages/core/src/models/modelsConfig.ts b/packages/core/src/models/modelsConfig.ts
index d22cc790c..f7086bbca 100644
--- a/packages/core/src/models/modelsConfig.ts
+++ b/packages/core/src/models/modelsConfig.ts
@@ -301,6 +301,17 @@ export class ModelsConfig {
return this.modelRegistry.hasModel(authType, modelId);
}
+ /**
+ * Get a fully resolved provider model config for the given authType/modelId.
+ * Returns undefined for raw runtime models that are not present in the registry.
+ */
+ getResolvedModel(
+ authType: AuthType,
+ modelId: string,
+ ): ResolvedModelConfig | undefined {
+ return this.modelRegistry.getModel(authType, modelId);
+ }
+
/**
* Set model programmatically (e.g., VLM auto-switch, fallback).
* Supports both registry models and raw model IDs.
diff --git a/packages/core/src/subagents/model-selection.test.ts b/packages/core/src/subagents/model-selection.test.ts
new file mode 100644
index 000000000..aaa2624f8
--- /dev/null
+++ b/packages/core/src/subagents/model-selection.test.ts
@@ -0,0 +1,44 @@
+/**
+ * @license
+ * Copyright 2025 Qwen
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, expect, it } from 'vitest';
+import { AuthType } from '../core/contentGenerator.js';
+import { parseSubagentModelSelection } from './model-selection.js';
+
+describe('parseSubagentModelSelection', () => {
+ it('treats omitted models as inherit', () => {
+ expect(parseSubagentModelSelection(undefined)).toEqual({
+ inherits: true,
+ });
+ });
+
+ it('treats explicit inherit as inherit', () => {
+ expect(parseSubagentModelSelection('inherit')).toEqual({
+ inherits: true,
+ });
+ });
+
+ it('parses bare model IDs', () => {
+ expect(parseSubagentModelSelection('glm-5')).toEqual({
+ modelId: 'glm-5',
+ inherits: false,
+ });
+ });
+
+ it('parses authType-prefixed model IDs', () => {
+ expect(parseSubagentModelSelection('openai:glm-5')).toEqual({
+ authType: AuthType.USE_OPENAI,
+ modelId: 'glm-5',
+ inherits: false,
+ });
+ });
+
+ it('rejects invalid authType prefixes', () => {
+ expect(() => parseSubagentModelSelection('invalid:glm-5')).toThrow(
+ /Invalid authType prefix/,
+ );
+ });
+});
diff --git a/packages/core/src/subagents/model-selection.ts b/packages/core/src/subagents/model-selection.ts
new file mode 100644
index 000000000..cea1ef97d
--- /dev/null
+++ b/packages/core/src/subagents/model-selection.ts
@@ -0,0 +1,58 @@
+/**
+ * @license
+ * Copyright 2025 Qwen
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { AuthType } from '../core/contentGenerator.js';
+
+export interface ParsedSubagentModelSelection {
+ authType?: AuthType;
+ modelId?: string;
+ inherits: boolean;
+}
+
+const AUTH_TYPES = new Set(Object.values(AuthType));
+
+/**
+ * Parse a subagent model selector.
+ *
+ * Supported forms:
+ * - omitted / inherit -> use parent conversation model
+ * - modelId -> use parent authType with the provided modelId
+ * - authType:modelId -> use explicit authType and modelId
+ */
+export function parseSubagentModelSelection(
+ model: string | undefined,
+): ParsedSubagentModelSelection {
+ const trimmed = model?.trim();
+ if (!trimmed || trimmed === 'inherit') {
+ return { inherits: true };
+ }
+
+ const colonIndex = trimmed.indexOf(':');
+ if (colonIndex === -1) {
+ return { modelId: trimmed, inherits: false };
+ }
+
+ const maybeAuthType = trimmed.slice(0, colonIndex).trim();
+ const modelId = trimmed.slice(colonIndex + 1).trim();
+
+ if (!AUTH_TYPES.has(maybeAuthType as AuthType)) {
+ throw new Error(
+ `Invalid authType prefix "${maybeAuthType}". Expected one of: ${Object.values(AuthType).join(', ')}`,
+ );
+ }
+
+ if (!modelId) {
+ throw new Error(
+ 'Model selector must include a model ID after the authType',
+ );
+ }
+
+ return {
+ authType: maybeAuthType as AuthType,
+ modelId,
+ inherits: false,
+ };
+}
diff --git a/packages/core/src/subagents/subagent-manager.test.ts b/packages/core/src/subagents/subagent-manager.test.ts
index 5fb13e6e2..13b1c077b 100644
--- a/packages/core/src/subagents/subagent-manager.test.ts
+++ b/packages/core/src/subagents/subagent-manager.test.ts
@@ -83,11 +83,11 @@ describe('SubagentManager', () => {
tools: ['read_file', 'write_file'],
};
}
- if (yamlString.includes('modelConfig:')) {
+ if (yamlString.includes('model:')) {
return {
name: 'test-agent',
description: 'A test subagent',
- modelConfig: { model: 'custom-model', temp: 0.5 },
+ model: 'custom-model',
};
}
if (yamlString.includes('runConfig:')) {
@@ -130,17 +130,8 @@ describe('SubagentManager', () => {
for (const [key, value] of Object.entries(obj)) {
if (key === 'tools' && Array.isArray(value)) {
yaml += `tools:\n${value.map((tool) => ` - ${tool}`).join('\n')}\n`;
- } else if (
- key === 'modelConfig' &&
- typeof value === 'object' &&
- value
- ) {
- yaml += `modelConfig:\n`;
- for (const [k, v] of Object.entries(
- value as Record,
- )) {
- yaml += ` ${k}: ${v}\n`;
- }
+ } else if (key === 'model') {
+ yaml += `model: ${value}\n`;
} else if (key === 'runConfig' && typeof value === 'object' && value) {
yaml += `runConfig:\n`;
for (const [k, v] of Object.entries(
@@ -229,13 +220,11 @@ You are a helpful assistant.
expect(config.tools).toEqual(['read_file', 'write_file']);
});
- it('should parse content with model config', () => {
+ it('should parse content with model selector', () => {
const markdownWithModel = `---
name: test-agent
description: A test subagent
-modelConfig:
- model: custom-model
- temp: 0.5
+model: custom-model
---
You are a helpful assistant.
@@ -247,7 +236,33 @@ You are a helpful assistant.
'project',
);
- expect(config.modelConfig).toEqual({ model: 'custom-model', temp: 0.5 });
+ expect(config.model).toBe('custom-model');
+ });
+
+ it('should parse legacy modelConfig frontmatter for compatibility', () => {
+ const markdownWithLegacyModel = `---
+name: test-agent
+description: A test subagent
+modelConfig:
+ model: legacy-model
+---
+
+You are a helpful assistant.
+`;
+
+ mockParseYaml.mockReturnValueOnce({
+ name: 'test-agent',
+ description: 'A test subagent',
+ modelConfig: { model: 'legacy-model' },
+ });
+
+ const config = manager.parseSubagentContent(
+ markdownWithLegacyModel,
+ validConfig.filePath!,
+ 'project',
+ );
+
+ expect(config.model).toBe('legacy-model');
});
it('should parse content with run config', () => {
@@ -419,24 +434,22 @@ You are a helpful assistant.
expect(serialized).toContain('- write_file');
});
- it('should serialize configuration with model config', () => {
+ it('should serialize configuration with model selector', () => {
const configWithModel: SubagentConfig = {
...validConfig,
- modelConfig: { model: 'custom-model', temp: 0.5 },
+ model: 'custom-model',
};
const serialized = manager.serializeSubagent(configWithModel);
- expect(serialized).toContain('modelConfig:');
expect(serialized).toContain('model: custom-model');
- expect(serialized).toContain('temp: 0.5');
});
it('should not include empty optional fields', () => {
const serialized = manager.serializeSubagent(validConfig);
expect(serialized).not.toContain('tools:');
- expect(serialized).not.toContain('modelConfig:');
+ expect(serialized).not.toContain('model:');
expect(serialized).not.toContain('runConfig:');
});
});
@@ -1104,26 +1117,28 @@ System prompt 3`);
]);
});
- it('should merge custom model and run configurations', () => {
+ it('should set modelConfig.model from model selector and merge run configurations', () => {
const configWithCustom: SubagentConfig = {
...validConfig,
- modelConfig: { model: 'custom-model', temp: 0.5 },
+ model: 'custom-model',
runConfig: { max_time_minutes: 5 },
};
const runtimeConfig = manager.convertToRuntimeConfig(configWithCustom);
expect(runtimeConfig.modelConfig.model).toBe('custom-model');
- expect(runtimeConfig.modelConfig.temp).toBe(0.5);
expect(runtimeConfig.runConfig.max_time_minutes).toBe(5);
- // No default values are provided anymore
- expect(Object.keys(runtimeConfig.modelConfig)).toEqual([
- 'model',
- 'temp',
- ]);
- expect(Object.keys(runtimeConfig.runConfig)).toEqual([
- 'max_time_minutes',
- ]);
+ });
+
+ it('should reject cross-provider model selectors', () => {
+ const configWithCrossProvider: SubagentConfig = {
+ ...validConfig,
+ model: 'openai:gpt-4',
+ };
+
+ expect(() =>
+ manager.convertToRuntimeConfig(configWithCrossProvider),
+ ).toThrow(/Cross-provider model selectors/);
});
});
@@ -1144,19 +1159,18 @@ System prompt 3`);
it('should merge nested configurations', () => {
const configWithNested: SubagentConfig = {
...validConfig,
- modelConfig: { model: 'original-model', temp: 0.7 },
+ model: 'original-model',
runConfig: { max_time_minutes: 10, max_turns: 20 },
};
const updates = {
- modelConfig: { temp: 0.5 },
+ model: 'updated-model',
runConfig: { max_time_minutes: 5 },
};
const merged = manager.mergeConfigurations(configWithNested, updates);
- expect(merged.modelConfig!.model).toBe('original-model'); // Should keep original
- expect(merged.modelConfig!.temp).toBe(0.5); // Should update
+ expect(merged.model).toBe('updated-model');
expect(merged.runConfig!.max_time_minutes).toBe(5); // Should update
expect(merged.runConfig!.max_turns).toBe(20); // Should keep original
});
diff --git a/packages/core/src/subagents/subagent-manager.ts b/packages/core/src/subagents/subagent-manager.ts
index a3928041d..5ec87de9c 100644
--- a/packages/core/src/subagents/subagent-manager.ts
+++ b/packages/core/src/subagents/subagent-manager.ts
@@ -36,6 +36,7 @@ import type {
import type { Config } from '../config/config.js';
import { createDebugLogger } from '../utils/debugLogger.js';
import { normalizeContent } from '../utils/textUtils.js';
+import { parseSubagentModelSelection } from './model-selection.js';
const debugLogger = createDebugLogger('SUBAGENT_MANAGER');
import { BuiltinAgentRegistry } from './builtin-agents.js';
@@ -568,10 +569,8 @@ export class SubagentManager {
frontmatter['tools'] = config.tools;
}
- // No outputs section
-
- if (config.modelConfig) {
- frontmatter['modelConfig'] = config.modelConfig;
+ if (config.model && config.model !== 'inherit') {
+ frontmatter['model'] = config.model;
}
if (config.runConfig) {
@@ -640,25 +639,28 @@ export class SubagentManager {
* @returns Runtime configuration for AgentHeadless
*/
convertToRuntimeConfig(config: SubagentConfig): SubagentRuntimeConfig {
- // Build prompt configuration
const promptConfig: PromptConfig = {
systemPrompt: config.systemPrompt,
};
- // Build model configuration
+ const selection = parseSubagentModelSelection(config.model);
+ if (selection.authType) {
+ throw new SubagentError(
+ `Cross-provider model selectors (e.g. "${config.model}") are not supported for subagents. Use a bare model ID instead, or use Arena for cross-provider agents.`,
+ SubagentErrorCode.INVALID_CONFIG,
+ config.name,
+ );
+ }
const modelConfig: ModelConfig = {
- ...config.modelConfig,
+ ...(selection.modelId ? { model: selection.modelId } : {}),
};
- // Build run configuration
const runConfig: RunConfig = {
...config.runConfig,
};
- // Build tool configuration if tools are specified
let toolConfig: ToolConfig | undefined;
if (config.tools && config.tools.length > 0) {
- // Transform tools array to ensure all entries are tool names (not display names)
const toolNames = this.transformToToolNames(config.tools);
toolConfig = {
tools: toolNames,
@@ -740,10 +742,6 @@ export class SubagentManager {
return {
...base,
...updates,
- // Handle nested objects specially
- modelConfig: updates.modelConfig
- ? { ...base.modelConfig, ...updates.modelConfig }
- : base.modelConfig,
runConfig: updates.runConfig
? { ...base.runConfig, ...updates.runConfig }
: base.runConfig,
@@ -956,13 +954,20 @@ function parseSubagentContent(
// Extract optional fields
const tools = frontmatter['tools'] as string[] | undefined;
- const modelConfig = frontmatter['modelConfig'] as
+ const modelRaw = frontmatter['model'];
+ const legacyModelConfig = frontmatter['modelConfig'] as
| Record
| undefined;
const runConfig = frontmatter['runConfig'] as
| Record
| undefined;
const color = frontmatter['color'] as string | undefined;
+ const model =
+ modelRaw != null && modelRaw !== ''
+ ? String(modelRaw)
+ : typeof legacyModelConfig?.['model'] === 'string'
+ ? legacyModelConfig['model']
+ : undefined;
const config: SubagentConfig = {
name,
@@ -970,7 +975,7 @@ function parseSubagentContent(
tools,
systemPrompt: systemPrompt.trim(),
filePath,
- modelConfig: modelConfig as Partial,
+ model,
runConfig: runConfig as Partial,
color,
level,
diff --git a/packages/core/src/subagents/types.ts b/packages/core/src/subagents/types.ts
index 55e57f61e..3e67869a5 100644
--- a/packages/core/src/subagents/types.ts
+++ b/packages/core/src/subagents/types.ts
@@ -64,10 +64,12 @@ export interface SubagentConfig {
filePath?: string;
/**
- * Optional model configuration. If not provided, uses defaults.
- * Can specify model name, temperature, and top_p values.
+ * Optional model selector.
+ * - Omitted or 'inherit': use the main conversation model
+ * - 'model-id': use the given model with the main conversation authType
+ * - 'authType:model-id': use the given authType and model ID
*/
- modelConfig?: Partial;
+ model?: string;
/**
* Optional runtime configuration. If not provided, uses defaults.
diff --git a/packages/core/src/subagents/validation.test.ts b/packages/core/src/subagents/validation.test.ts
index 1d705cc0d..0818ef4cd 100644
--- a/packages/core/src/subagents/validation.test.ts
+++ b/packages/core/src/subagents/validation.test.ts
@@ -224,54 +224,53 @@ describe('SubagentValidator', () => {
});
});
- describe('validateModelConfig', () => {
- it('should accept valid model configurations', () => {
- const validConfigs = [
- { model: 'gemini-1.5-pro', temp: 0.7, top_p: 0.9 },
- { temp: 0.5 },
- { top_p: 1.0 },
- {},
- ];
+ describe('validateModel', () => {
+ it('should accept valid model selectors', () => {
+ const validModels = ['inherit', 'glm-5', 'claude-sonnet-4-6'];
- for (const config of validConfigs) {
- const result = validator.validateModelConfig(config);
+ for (const model of validModels) {
+ const result = validator.validateModel(model);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
}
});
- it('should reject invalid model names', () => {
- const result = validator.validateModelConfig({ model: '' });
- expect(result.isValid).toBe(false);
- expect(result.errors).toContain('Model name must be a non-empty string');
- });
+ it('should reject cross-provider authType-prefixed selectors', () => {
+ const crossProviderModels = ['openai:glm-5', 'anthropic:sonnet'];
- it('should reject invalid temperature values', () => {
- const invalidTemps = [-0.1, 2.1, 'not-a-number'];
-
- for (const temp of invalidTemps) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const result = validator.validateModelConfig({ temp: temp as any });
+ for (const model of crossProviderModels) {
+ const result = validator.validateModel(model);
expect(result.isValid).toBe(false);
+ expect(result.errors[0]).toContain('Cross-provider model selectors');
}
});
- it('should warn about high temperature', () => {
- const result = validator.validateModelConfig({ temp: 1.5 });
- expect(result.isValid).toBe(true);
- expect(result.warnings).toContain(
- 'High temperature (>1) may produce very creative but unpredictable results',
+ it('should reject empty model selectors', () => {
+ const result = validator.validateModel('');
+ expect(result.isValid).toBe(false);
+ expect(result.errors).toContain('Model must be a non-empty string');
+ });
+
+ it('should reject invalid authType prefixes', () => {
+ const result = validator.validateModel('invalid:glm-5');
+ expect(result.isValid).toBe(false);
+ expect(result.errors[0]).toContain('Invalid authType prefix');
+ });
+
+ it('should reject missing model IDs after authType prefixes', () => {
+ const result = validator.validateModel('openai:');
+ expect(result.isValid).toBe(false);
+ expect(result.errors).toContain(
+ 'Model selector must include a model ID after the authType',
);
});
- it('should reject invalid top_p values', () => {
- const invalidTopP = [-0.1, 1.1, 'not-a-number'];
-
- for (const top_p of invalidTopP) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const result = validator.validateModelConfig({ top_p: top_p as any });
- expect(result.isValid).toBe(false);
- }
+ it('should warn when inherit is explicit', () => {
+ const result = validator.validateModel('inherit');
+ expect(result.isValid).toBe(true);
+ expect(result.warnings).toContain(
+ 'Explicit "inherit" is optional because omitting the model uses the main conversation model',
+ );
});
});
diff --git a/packages/core/src/subagents/validation.ts b/packages/core/src/subagents/validation.ts
index 15fb31269..e2b4070ad 100644
--- a/packages/core/src/subagents/validation.ts
+++ b/packages/core/src/subagents/validation.ts
@@ -6,7 +6,8 @@
import { SubagentError, SubagentErrorCode } from './types.js';
import type { SubagentConfig, ValidationResult } from './types.js';
-import type { ModelConfig, RunConfig } from '../agents/runtime/agent-types.js';
+import type { RunConfig } from '../agents/runtime/agent-types.js';
+import { parseSubagentModelSelection } from './model-selection.js';
/**
* Validates subagent configurations to ensure they are well-formed
@@ -54,9 +55,9 @@ export class SubagentValidator {
warnings.push(...toolsValidation.warnings);
}
- // Validate model config if specified
- if (config.modelConfig) {
- const modelValidation = this.validateModelConfig(config.modelConfig);
+ // Validate model selector if specified
+ if (config.model) {
+ const modelValidation = this.validateModel(config.model);
if (!modelValidation.isValid) {
errors.push(...modelValidation.errors);
}
@@ -240,42 +241,39 @@ export class SubagentValidator {
}
/**
- * Validates model configuration.
+ * Validates a subagent model selector.
*
- * @param modelConfig - Partial model configuration to validate
+ * @param model - Model selector to validate
* @returns ValidationResult
*/
- validateModelConfig(modelConfig: ModelConfig): ValidationResult {
+ validateModel(model: string): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
- if (modelConfig.model !== undefined) {
- if (
- typeof modelConfig.model !== 'string' ||
- modelConfig.model.trim().length === 0
- ) {
- errors.push('Model name must be a non-empty string');
- }
+ if (typeof model !== 'string' || model.trim().length === 0) {
+ errors.push('Model must be a non-empty string');
+ return {
+ isValid: false,
+ errors,
+ warnings,
+ };
}
- if (modelConfig.temp !== undefined) {
- if (typeof modelConfig.temp !== 'number') {
- errors.push('Temperature must be a number');
- } else if (modelConfig.temp < 0 || modelConfig.temp > 2) {
- errors.push('Temperature must be between 0 and 2');
- } else if (modelConfig.temp > 1) {
- warnings.push(
- 'High temperature (>1) may produce very creative but unpredictable results',
+ try {
+ const selection = parseSubagentModelSelection(model);
+ if (selection.authType) {
+ errors.push(
+ `Cross-provider model selectors (e.g. "${model}") are not yet supported for subagents. Use a bare model ID instead.`,
);
}
+ } catch (error) {
+ errors.push(error instanceof Error ? error.message : 'Invalid model');
}
- if (modelConfig.top_p !== undefined) {
- if (typeof modelConfig.top_p !== 'number') {
- errors.push('top_p must be a number');
- } else if (modelConfig.top_p < 0 || modelConfig.top_p > 1) {
- errors.push('top_p must be between 0 and 1');
- }
+ if (model.trim() === 'inherit') {
+ warnings.push(
+ 'Explicit "inherit" is optional because omitting the model uses the main conversation model',
+ );
}
return {
diff --git a/packages/sdk-java/qwencode/src/main/java/com/alibaba/qwen/code/cli/protocol/protocol.ts b/packages/sdk-java/qwencode/src/main/java/com/alibaba/qwen/code/cli/protocol/protocol.ts
index e5eeb1212..2a7e4fcbf 100644
--- a/packages/sdk-java/qwencode/src/main/java/com/alibaba/qwen/code/cli/protocol/protocol.ts
+++ b/packages/sdk-java/qwencode/src/main/java/com/alibaba/qwen/code/cli/protocol/protocol.ts
@@ -534,12 +534,6 @@ export function isToolResultBlock(block: any): block is ToolResultBlock {
export type SubagentLevel = 'session';
-export interface ModelConfig {
- model?: string;
- temp?: number;
- top_p?: number;
-}
-
export interface RunConfig {
max_time_minutes?: number;
max_turns?: number;
@@ -552,7 +546,7 @@ export interface SubagentConfig {
systemPrompt: string;
level: SubagentLevel;
filePath?: string;
- modelConfig?: Partial;
+ model?: string;
runConfig?: Partial;
color?: string;
readonly isBuiltin?: boolean;
diff --git a/packages/sdk-typescript/src/index.ts b/packages/sdk-typescript/src/index.ts
index 805d03cfb..7d841fe5a 100644
--- a/packages/sdk-typescript/src/index.ts
+++ b/packages/sdk-typescript/src/index.ts
@@ -36,7 +36,6 @@ export type {
ControlCancelRequest,
SubagentConfig,
SubagentLevel,
- ModelConfig,
RunConfig,
} from './types/protocol.js';
diff --git a/packages/sdk-typescript/src/types/protocol.ts b/packages/sdk-typescript/src/types/protocol.ts
index 3b242d08f..33b0f53d4 100644
--- a/packages/sdk-typescript/src/types/protocol.ts
+++ b/packages/sdk-typescript/src/types/protocol.ts
@@ -545,12 +545,6 @@ export function isToolResultBlock(block: any): block is ToolResultBlock {
export type SubagentLevel = 'session';
-export interface ModelConfig {
- model?: string;
- temp?: number;
- top_p?: number;
-}
-
export interface RunConfig {
max_time_minutes?: number;
max_turns?: number;
@@ -563,7 +557,7 @@ export interface SubagentConfig {
systemPrompt: string;
level: SubagentLevel;
filePath?: string;
- modelConfig?: Partial;
+ model?: string;
runConfig?: Partial;
color?: string;
readonly isBuiltin?: boolean;
diff --git a/packages/sdk-typescript/src/types/queryOptionsSchema.ts b/packages/sdk-typescript/src/types/queryOptionsSchema.ts
index 823bc7085..5d1d07004 100644
--- a/packages/sdk-typescript/src/types/queryOptionsSchema.ts
+++ b/packages/sdk-typescript/src/types/queryOptionsSchema.ts
@@ -94,12 +94,6 @@ export const McpServerConfigSchema = z.union([
SdkMcpServerConfigSchema,
]);
-export const ModelConfigSchema = z.object({
- model: z.string().optional(),
- temp: z.number().optional(),
- top_p: z.number().optional(),
-});
-
export const RunConfigSchema = z.object({
max_time_minutes: z.number().optional(),
max_turns: z.number().optional(),
@@ -110,7 +104,7 @@ export const SubagentConfigSchema = z.object({
description: z.string().min(1, 'Description must be a non-empty string'),
tools: z.array(z.string()).optional(),
systemPrompt: z.string().min(1, 'System prompt must be a non-empty string'),
- modelConfig: ModelConfigSchema.partial().optional(),
+ model: z.string().optional(),
runConfig: RunConfigSchema.partial().optional(),
color: z.string().optional(),
isBuiltin: z.boolean().optional(),