Time: 10 minutes
Prerequisite: Complete Step 7: Run and explain the application.
You'll list the models available to the signed-in user and use the selected model for the report session.
A model is the specific large language model the runtime sends each turn to. The models available through GitHub Copilot change over time and differ by account, so an application asks the runtime which ones it may use instead of hard-coding a name.
:::language dotnet
The Copilot runtime may expose more than one model. ListModelsAsync returns the models available
for the current account. SessionConfig.Model selects one when you create a session.
:::
:::language nodejs
The Copilot runtime may expose more than one model. client.listModels() returns the models
available for the current account. Pass the chosen id as model when you call createSession.
:::
:::language python
The Copilot runtime may expose more than one model. await client.list_models() returns the models
available for the current account. Pass the chosen id as model when you call create_session.
:::
:::language go
The Copilot runtime may expose more than one model. client.ListModels(ctx) returns the models
available for the current account. Set SessionConfig.Model when you create a session.
:::
:::language rust
The Copilot runtime may expose more than one model. client.list_models().await? returns the models
available for the current account (it uses models().list() under the hood). Set
SessionConfig.model when you create a session.
:::
:::language java
The Copilot runtime may expose more than one model. client.listModels() returns the models
available for the current account. Call SessionConfig.setModel(selectedId) when you create a
session.
:::
Changing the model can affect latency, capability, and billing. It does not change the local tools, MCP configuration, or permission policy, which is why this topic comes after the core architecture.
:::language dotnet
Model selection configures CopilotSession. It does not replace the client or either tool boundary.
:::
:::language nodejs
Model selection configures the session created by createSession. It does not replace the client or
either tool boundary.
:::
:::language python
Model selection configures the session created by create_session. It does not replace the client
or either tool boundary.
:::
:::language go
Model selection configures SessionConfig. It does not replace the client or either tool boundary.
:::
:::language rust
Model selection configures SessionConfig. It does not replace the client or either tool boundary.
:::
:::language java
Model selection configures SessionConfig. It does not replace the client or either tool boundary.
:::
:::language dotnet
Create Helpers/ModelSelector.cs:
using GitHub.Copilot;
namespace HelloCopilotSDK.Helpers;
public static class ModelSelector
{
public static async Task<string?> SelectAsync(CopilotClient client)
{
var models = (await client.ListModelsAsync())?.ToList();
if (models is null || models.Count is 0)
{
Console.WriteLine("No model list was returned; using the account default.");
return null;
}
Console.WriteLine("Available models:");
for (var index = 0; index < models.Count; index++)
{
Console.WriteLine($"{index + 1}. {models[index].Name}");
}
Console.Write($"Choose 1-{models.Count} [1]: ");
var valid = int.TryParse(Console.ReadLine(), out var choice) &&
choice >= 1 &&
choice <= models.Count;
var selected = models[(valid ? choice : 1) - 1];
Console.WriteLine($"Using {selected.Name}\n");
return selected.Id;
}
}:::
:::language dotnet
After PingAsync in Program.cs, insert:
var selectedModel = await ModelSelector.SelectAsync(client);:::
:::language dotnet
Then add Model = selectedModel to SessionConfig:
await using var session = await client.CreateSessionAsync(new SessionConfig
{
Model = selectedModel,
Streaming = true,
// Keep the existing permission, local-tool, and MCP configuration.
});::: Do not remove the rest of the Step 6 session configuration.
:::language nodejs
Create src/model-selector.ts:
import type { CopilotClient } from "@github/copilot-sdk";
import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
export async function selectModel(client: CopilotClient): Promise<string | undefined> {
const models = await client.listModels();
const [defaultModel] = models;
if (!defaultModel) {
console.log("No model list was returned; using the account default.");
return undefined;
}
console.log("Available models:");
models.forEach((model, index) => {
console.log(`${index + 1}. ${model.name}`);
});
const rl = createInterface({ input, output });
try {
const answer = (await rl.question(`Choose 1-${models.length} [1]: `)).trim();
const choice = Number.parseInt(answer, 10);
const selected =
Number.isInteger(choice) && choice >= 1 && choice <= models.length
? models[choice - 1] ?? defaultModel
: defaultModel;
console.log(`Using ${selected.name}\n`);
return selected.id;
} finally {
rl.close();
}
}In src/report.ts, import the helper and call it after client.start():
import { CopilotClient } from "@github/copilot-sdk";
import { selectModel } from "./model-selector.js";
import {
accessibilityRuleLookup,
createSnapshotReader,
permissionForTarget,
reportPrompt,
streamResponse,
} from "./workshop.js";
const input = process.argv[2];
if (!input) throw new Error("Usage: npm start -- <http-or-https-url>");
const target = new URL(input.includes("://") ? input : `https://${input}`);
if (!["http:", "https:"].includes(target.protocol)) {
throw new Error("Enter an absolute HTTP or HTTPS URL.");
}
const client = new CopilotClient();
await client.start();
try {
const selectedModel = await selectModel(client);
const session = await client.createSession({
model: selectedModel,
streaming: true,
onPermissionRequest: permissionForTarget(target),
tools: [accessibilityRuleLookup, createSnapshotReader(process.cwd())],
availableTools: [
"accessibility_rule_lookup",
"read_latest_accessibility_snapshot",
"playwright-browser_navigate",
],
mcpServers: {
playwright: {
command: "npx",
args: ["-y", "@playwright/mcp@0.0.78", "--browser=msedge", "--output-dir", ".playwright-mcp", "--output-mode", "file"],
workingDirectory: process.cwd(),
tools: ["browser_navigate"],
},
},
});
try {
await streamResponse(session, reportPrompt(target));
} finally {
await session.disconnect();
}
} finally {
await client.stop();
}Keep every existing tool, MCP, and permission setting from Step 6. Only add model: selectedModel.
:::
:::language python
Create model_selector.py:
from __future__ import annotations
from copilot import CopilotClient
async def select_model(client: CopilotClient) -> str | None:
models = await client.list_models()
if not models:
print("No model list was returned; using the account default.")
return None
print("Available models:")
for index, model in enumerate(models, start=1):
print(f"{index}. {model.name}")
answer = input(f"Choose 1-{len(models)} [1]: ").strip()
try:
choice = int(answer)
except ValueError:
choice = 1
if choice < 1 or choice > len(models):
choice = 1
selected = models[choice - 1]
print(f"Using {selected.name}\n")
return selected.idIn report.py, import the helper and pass model= into create_session without
removing the Step 6 tool configuration:
import asyncio
import sys
from urllib.parse import urlsplit
from copilot import CopilotClient
from copilot.session_events import (
AssistantMessageData,
AssistantMessageDeltaData,
SessionErrorData,
SessionIdleData,
ToolExecutionCompleteData,
ToolExecutionStartData,
)
from model_selector import select_model
from workshop import (
accessibility_rule_lookup,
create_snapshot_reader,
permission_for_target,
report_prompt,
)
async def main() -> None:
target = sys.argv[1] if len(sys.argv) == 2 else input("Enter URL to analyze: ").strip()
target = target if "://" in target else f"https://{target}"
if urlsplit(target).scheme not in {"http", "https"}:
raise ValueError("Enter an absolute HTTP or HTTPS URL.")
async with CopilotClient() as client:
selected_model = await select_model(client)
async with await client.create_session(
model=selected_model,
streaming=True,
on_permission_request=permission_for_target(target),
tools=[accessibility_rule_lookup, create_snapshot_reader(".")],
available_tools=[
"accessibility_rule_lookup",
"read_latest_accessibility_snapshot",
"playwright-browser_navigate",
],
mcp_servers={
"playwright": {
"command": "npx",
"args": ["-y", "@playwright/mcp@0.0.78", "--browser=msedge", "--output-dir", ".playwright-mcp", "--output-mode", "file"],
"working_directory": ".",
"tools": ["browser_navigate"],
}
},
) as session:
done = asyncio.Event()
error: RuntimeError | None = None
received_delta = False
def on_event(event) -> None:
nonlocal error, received_delta
match event.data:
case AssistantMessageDeltaData(delta_content=delta) if delta:
received_delta = True
print(delta, end="", flush=True)
case AssistantMessageData(content=content) if content and not received_delta:
print(content)
case ToolExecutionStartData(tool_name=name):
print(f"\n[tool:start] {name}")
case ToolExecutionCompleteData(success=success):
print(f"[tool:done] success={success}")
case SessionErrorData(message=message):
error = RuntimeError(message)
done.set()
case SessionIdleData():
done.set()
session.on(on_event)
await session.send(report_prompt(target))
await done.wait()
if error is not None:
raise error
if __name__ == "__main__":
asyncio.run(main())Keep launching through python main.py so the existing entrypoint still imports report.main.
:::
:::language go
Add this helper near the top of main.go (or in a sibling file in the same package):
func selectModel(ctx context.Context, client *copilot.Client) (string, error) {
models, err := client.ListModels(ctx)
if err != nil {
return "", err
}
if len(models) == 0 {
fmt.Println("No model list was returned; using the account default.")
return "", nil
}
fmt.Println("Available models:")
for index, model := range models {
fmt.Printf("%d. %s\n", index+1, model.Name)
}
fmt.Printf("Choose 1-%d [1]: ", len(models))
var answer string
fmt.Scanln(&answer)
choice := 1
if parsed, parseErr := strconv.Atoi(strings.TrimSpace(answer)); parseErr == nil {
choice = parsed
}
if choice < 1 || choice > len(models) {
choice = 1
}
selected := models[choice-1]
fmt.Printf("Using %s\n\n", selected.Name)
return selected.ID, nil
}Add "strconv" to the import block if it is not already present. After client.Start, select a
model and set SessionConfig.Model:
client := copilot.NewClient(&copilot.ClientOptions{LogLevel: "error"})
if err := client.Start(context.Background()); err != nil {
panic(err)
}
defer client.Stop()
selectedModel, err := selectModel(context.Background(), client)
if err != nil {
panic(err)
}
session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{
Model: selectedModel,
Streaming: copilot.Bool(true),
Tools: []copilot.Tool{lookup, readSnapshot},
AvailableTools: []string{"accessibility_rule_lookup", "read_latest_accessibility_snapshot", "playwright-browser_navigate"},
OnPermissionRequest: permissionForTarget(target),
MCPServers: map[string]copilot.MCPServerConfig{
"playwright": copilot.MCPStdioServerConfig{
Command: "npx",
Args: []string{"-y", "@playwright/mcp@0.0.78", "--browser=msedge", "--output-dir", ".playwright-mcp", "--output-mode", "file"},
WorkingDirectory: workingDirectory,
Tools: []string{"browser_navigate"},
},
},
})Keep every existing tool, MCP, and permission setting from Step 6. Only add Model: selectedModel.
:::
:::language rust
Add this helper in src/main.rs:
async fn select_model(client: &Client) -> Result<Option<String>, Box<dyn std::error::Error>> {
// list_models caches the catalog; it calls models().list() on first use.
let models = client.list_models().await?;
if models.is_empty() {
println!("No model list was returned; using the account default.");
return Ok(None);
}
println!("Available models:");
for (index, model) in models.iter().enumerate() {
println!("{}. {}", index + 1, model.name);
}
print!("Choose 1-{} [1]: ", models.len());
io::stdout().flush()?;
let mut answer = String::new();
io::stdin().read_line(&mut answer)?;
let choice = answer.trim().parse::<usize>().unwrap_or(1);
let index = if (1..=models.len()).contains(&choice) {
choice - 1
} else {
0
};
let selected = &models[index];
println!("Using {}\n", selected.name);
Ok(Some(selected.id.clone()))
}After Client::start, select a model and set config.model before create_session:
let client = Client::start(ClientOptions::default()).await?;
let selected_model = select_model(&client).await?;
let mut config = SessionConfig::default();
config.model = selected_model;
config.streaming = Some(true);
config.tools = Some(vec![lookup, reader]);
config.available_tools = Some(vec![
"accessibility_rule_lookup".to_owned(),
"read_latest_accessibility_snapshot".to_owned(),
"playwright-browser_navigate".to_owned(),
]);
config.mcp_servers = Some(IndexMap::from([(
"playwright".to_owned(),
McpServerConfig::Stdio(McpStdioServerConfig {
command: "npx".to_owned(),
args: vec![
"-y".to_owned(),
"@playwright/mcp@0.0.78".to_owned(),
"--browser=msedge".to_owned(),
"--output-dir".to_owned(),
".playwright-mcp".to_owned(),
"--output-mode".to_owned(),
"file".to_owned(),
],
tools: Some(vec!["browser_navigate".to_owned()]),
working_directory: Some(working_directory.display().to_string()),
..Default::default()
}),
)]));
let config = config.with_permission_handler(Arc::new(ScopedPermissions {
target: target.clone(),
}));
let session = client.create_session(config).await?;Keep every existing tool, MCP, and permission setting from Step 6. Only add config.model.
:::
:::language java
Create src/main/java/workshop/ModelSelector.java:
package workshop;
import com.github.copilot.CopilotClient;
import com.github.copilot.rpc.ModelInfo;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.List;
public final class ModelSelector {
private ModelSelector() {
}
public static String select(CopilotClient client) throws Exception {
List<ModelInfo> models = client.listModels().get();
if (models == null || models.isEmpty()) {
System.out.println("No model list was returned; using the account default.");
return null;
}
System.out.println("Available models:");
for (int index = 0; index < models.size(); index++) {
System.out.println((index + 1) + ". " + models.get(index).getName());
}
System.out.print("Choose 1-" + models.size() + " [1]: ");
BufferedReader reader = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8));
String answer = reader.readLine();
int choice = 1;
try {
if (answer != null && !answer.isBlank()) {
choice = Integer.parseInt(answer.trim());
}
} catch (NumberFormatException ignored) {
choice = 1;
}
if (choice < 1 || choice > models.size()) {
choice = 1;
}
ModelInfo selected = models.get(choice - 1);
System.out.println("Using " + selected.getName() + System.lineSeparator());
return selected.getId();
}
}In src/main/java/workshop/AccessibilityReport.java, after client.start().get(),
select a model and call SessionConfig.setModel(selectedId):
try (var client = new CopilotClient()) {
client.start().get();
String selectedModel = ModelSelector.select(client);
var config = new SessionConfig()
.setModel(selectedModel)
.setStreaming(true)
.setTools(List.of(lookup, readSnapshot))
.setAvailableTools(List.of(
"accessibility_rule_lookup",
"read_latest_accessibility_snapshot",
"playwright-browser_navigate"))
.setMcpServers(Map.of("playwright", new McpStdioServerConfig()
.setCommand("npx")
.setArgs(List.of("-y", "@playwright/mcp@0.0.78", "--browser=msedge", "--output-dir", ".playwright-mcp", "--output-mode", "file"))
.setWorkingDirectory(workingDirectory.toString())
.setTools(List.of("browser_navigate"))))
.setOnPermissionRequest((request, ignored) -> {
if ("mcp".equals(request.getKind())
&& isExactNavigation(request.getExtensionData(), target)) {
return java.util.concurrent.CompletableFuture.completedFuture(
PermissionRequestResult.approveOnce());
}
if (options.allowLocalDemoMcp() && "mcp".equals(request.getKind())) {
return java.util.concurrent.CompletableFuture.completedFuture(
PermissionRequestResult.approveOnce());
}
return java.util.concurrent.CompletableFuture.completedFuture(
PermissionRequestResult.reject(
"This workshop allows Playwright to navigate only to the exact requested target. "
+ "MCP requests without target data remain denied unless the explicit "
+ LOCAL_DEMO_MCP_FLAG + " local-demo fallback is enabled."));
});
var session = client.createSession(config).get();
var response = session.sendAndWait(new MessageOptions().setPrompt(reportPrompt(target))).get();
if (response == null) {
throw new IllegalStateException("Copilot completed without an assistant message.");
}
System.out.println(response.getData().content());
}Keep every existing tool, MCP, and permission setting from Step 6. Only add
SessionConfig.setModel(selectedModel). In particular, preserve the default exact-target rejection
and the explicitly opted-in --allow-local-demo-mcp workaround for
github/copilot-sdk#2273; that fallback
approves only the mcp kind and cannot prove the target URL.
:::
:::language dotnet
dotnet run::: :::language nodejs
npm start -- "{{TARGET_APP_URL}}"::: :::language python
python main.py "{{TARGET_APP_URL}}"::: :::language go
go run . "{{TARGET_APP_URL}}"::: :::language rust
cargo run -- "{{TARGET_APP_URL}}"::: :::language java
./mvnw compile exec:java -Dexec.args="--allow-local-demo-mcp {{TARGET_APP_URL}}"::: Enter the workshop target URL, then choose a model, and confirm the same scoped tools still run.
Troubleshooting this step
| Symptom | Fix |
|---|---|
| No models are listed | The helper falls back to the account default; verify authentication if this is unexpected. |
| A number is outside the range | The helper safely uses the first model. |
| Tools disappear | Add only the model selection; retain the existing tool, MCP, and permission configuration. |
| Authentication error while listing models | Run copilot login again, then rerun the application. |
This step is complete when: the selected model is named and the report still uses both scoped tool types.
Why was model selection moved out of Step 1?
Check your answer
Model selection is configuration rather than a core agent concept. Leaving it until the end gets you to a useful Copilot response sooner and keeps the first lesson focused on clients and sessions.
- Bring your own key: pointing a session at your own OpenAI, Azure, or Anthropic credentials and models.
- SDK and CLI compatibility: which options each SDK exposes, including model listing and system messages.
- Azure managed identity: reaching Microsoft Foundry models without storing a key in the application.
Continue to Step 9: Generate an interactive HTML report.