Skip to content

Commit 26d74ac

Browse files
Merge pull request #6 from github/kdg/support-model-selection
SDK supports model selection
2 parents 25032c6 + 897299e commit 26d74ac

6 files changed

Lines changed: 210 additions & 19 deletions

File tree

cli/cmd/engine-cli/main.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ var (
3333
commitLogin string
3434
commitEmail string
3535
assignmentID string
36+
enableModelSelection bool
37+
selectedModel string
38+
defaultModel string
39+
availableModels []string
40+
modelVendors []string
3641
)
3742

3843
func main() {
@@ -88,6 +93,11 @@ func init() {
8893
runCmd.Flags().StringVar(&commitLogin, "commit-login", "engine-cli-user", "Git author name for commits")
8994
runCmd.Flags().StringVar(&commitEmail, "commit-email", "engine-cli@users.noreply.github.com", "Git author email for commits")
9095
runCmd.Flags().StringVar(&assignmentID, "assignment-id", "", "Assignment ID to enable cross-run history persistence")
96+
runCmd.Flags().BoolVar(&enableModelSelection, "enable-model-selection", false, "Enable the model selection feature flag in the job response")
97+
runCmd.Flags().StringVar(&selectedModel, "selected-model", "", "Selected model for this job")
98+
runCmd.Flags().StringVar(&defaultModel, "default-model", "", "Default model for this engine")
99+
runCmd.Flags().StringSliceVar(&availableModels, "available-model", nil, "Available model for this engine (repeatable)")
100+
runCmd.Flags().StringSliceVar(&modelVendors, "model-vendor", nil, "Model vendor for filtering (repeatable, e.g. Anthropic, OpenAI)")
91101

92102
_ = runCmd.MarkFlagRequired("repo")
93103
}
@@ -149,6 +159,11 @@ func runEngine(cmd *cobra.Command, args []string) error {
149159
BranchName: branchName,
150160
CommitLogin: commitLogin,
151161
CommitEmail: commitEmail,
162+
EnableModelSelection: enableModelSelection,
163+
SelectedModel: selectedModel,
164+
DefaultModel: defaultModel,
165+
AvailableModels: availableModels,
166+
ModelVendors: modelVendors,
152167
}
153168

154169
prNumber := setup.PRNumber
@@ -267,6 +282,13 @@ func runEngine(cmd *cobra.Command, args []string) error {
267282
GitToken: githubToken,
268283
}
269284

285+
if enableModelSelection {
286+
env.SelectedModel = selectedModel
287+
env.DefaultModel = defaultModel
288+
env.AvailableModels = availableModels
289+
env.ModelVendors = modelVendors
290+
}
291+
270292
result := runner.Run(ctx, command, env, runner.Options{WorkingDir: workingDir}, runnerCallbacks)
271293

272294
// Summary

cli/internal/runner/runner.go

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package runner
66
import (
77
"bufio"
88
"context"
9+
"encoding/json"
910
"fmt"
1011
"os"
1112
"os/exec"
@@ -15,13 +16,17 @@ import (
1516

1617
// Environment contains the platform environment variables for the engine.
1718
type Environment struct {
18-
JobID string
19-
APIToken string
20-
APIURL string
21-
JobNonce string
22-
InferenceToken string
23-
InferenceURL string
24-
GitToken string
19+
JobID string
20+
APIToken string
21+
APIURL string
22+
JobNonce string
23+
InferenceToken string
24+
InferenceURL string
25+
GitToken string
26+
SelectedModel string
27+
DefaultModel string
28+
AvailableModels []string
29+
ModelVendors []string
2530
}
2631

2732
// Callbacks contains optional callbacks for runner events.
@@ -124,18 +129,39 @@ func buildEnv(env Environment, extra map[string]string) []string {
124129
// Add platform environment variables
125130
// Note: We use GITHUB_* prefix for consistency with GitHub platform conventions
126131
platformVars := map[string]string{
127-
"GITHUB_JOB_ID": env.JobID,
128-
"GITHUB_JOB_NONCE": env.JobNonce,
129-
"GITHUB_PLATFORM_API_TOKEN": env.APIToken,
130-
"GITHUB_PLATFORM_API_URL": env.APIURL,
131-
"GITHUB_INFERENCE_TOKEN": env.InferenceToken,
132-
"GITHUB_GIT_TOKEN": env.GitToken,
132+
"GITHUB_JOB_ID": env.JobID,
133+
"GITHUB_JOB_NONCE": env.JobNonce,
134+
"GITHUB_PLATFORM_API_TOKEN": env.APIToken,
135+
"GITHUB_PLATFORM_API_URL": env.APIURL,
136+
"GITHUB_INFERENCE_TOKEN": env.InferenceToken,
137+
"GITHUB_GIT_TOKEN": env.GitToken,
133138
}
134139

135140
if env.InferenceURL != "" {
136141
platformVars["GITHUB_INFERENCE_URL"] = env.InferenceURL
137142
}
138143

144+
if env.SelectedModel != "" {
145+
platformVars["GITHUB_SELECTED_MODEL"] = env.SelectedModel
146+
}
147+
148+
if env.DefaultModel != "" {
149+
platformVars["GITHUB_DEFAULT_MODEL"] = env.DefaultModel
150+
}
151+
152+
if len(env.AvailableModels) > 0 {
153+
// json.Marshal cannot fail for []string, but handle the error defensively.
154+
if encoded, err := json.Marshal(env.AvailableModels); err == nil {
155+
platformVars["GITHUB_AVAILABLE_MODELS"] = string(encoded)
156+
}
157+
}
158+
159+
if len(env.ModelVendors) > 0 {
160+
if encoded, err := json.Marshal(env.ModelVendors); err == nil {
161+
platformVars["GITHUB_MODEL_VENDORS"] = string(encoded)
162+
}
163+
}
164+
139165
for k, v := range platformVars {
140166
result = append(result, fmt.Sprintf("%s=%s", k, v))
141167
}

cli/internal/server/server.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ type JobConfig struct {
3131
CommitLogin string
3232
CommitEmail string
3333
MCPProxyURL string
34+
EnableModelSelection bool
35+
SelectedModel string
36+
DefaultModel string
37+
AvailableModels []string
38+
ModelVendors []string
3439
}
3540

3641
// ProgressEvent represents a progress event received from an engine.
@@ -129,8 +134,8 @@ func (s *MockPlatformServer) Stop(ctx context.Context) error {
129134
}
130135

131136
var (
132-
getJobRegex = regexp.MustCompile(`^/agent/jobs/([^/]+)$`)
133-
progressRegex = regexp.MustCompile(`^/agent/jobs/([^/]+)/progress$`)
137+
getJobRegex = regexp.MustCompile(`^/agent/jobs/([^/]+)$`)
138+
progressRegex = regexp.MustCompile(`^/agent/jobs/([^/]+)/progress$`)
134139
)
135140

136141
func (s *MockPlatformServer) handleRequest(w http.ResponseWriter, r *http.Request) {
@@ -206,6 +211,28 @@ func (s *MockPlatformServer) handleGetJob(w http.ResponseWriter, r *http.Request
206211
response["mcp_proxy_url"] = s.jobConfig.MCPProxyURL
207212
}
208213

214+
if s.jobConfig.EnableModelSelection {
215+
response["features"] = map[string]any{
216+
"model_selection": true,
217+
}
218+
219+
if s.jobConfig.SelectedModel != "" {
220+
response["selected_model"] = s.jobConfig.SelectedModel
221+
}
222+
223+
if s.jobConfig.DefaultModel != "" {
224+
response["default_model"] = s.jobConfig.DefaultModel
225+
}
226+
227+
if len(s.jobConfig.AvailableModels) > 0 {
228+
response["available_models"] = s.jobConfig.AvailableModels
229+
}
230+
231+
if len(s.jobConfig.ModelVendors) > 0 {
232+
response["model_vendors"] = s.jobConfig.ModelVendors
233+
}
234+
}
235+
209236
if s.callbacks.OnJobFetched != nil {
210237
s.callbacks.OnJobFetched()
211238
}

docs/integration-guide.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ author: 'Your Name'
7575
# The fully qualified command to run the engine.
7676
# The platform executes this command directly — no implicit runtime setup.
7777
entrypoint: 'node --enable-source-maps dist/index.js'
78+
79+
# Optional: Specify the model vendors for model selection (e.g. 'Anthropic', 'OpenAI').
80+
# When set, the platform uses these to determine the available models for the engine.
81+
vendors:
82+
- 'Anthropic'
83+
- 'OpenAI'
7884
```
7985
8086
> **Note:** This is not a GitHub Action. The platform reads `entrypoint` from `engine.yaml` and runs it directly. All paths in the entrypoint are resolved relative to the engine's root directory.
@@ -92,6 +98,10 @@ The platform injects these environment variables into the engine process at runt
9298
| `GITHUB_INFERENCE_TOKEN` | Yes | Token used by your inference client / SDK for model calls. |
9399
| `GITHUB_INFERENCE_URL` | Yes | Base URL for the inference API (e.g. Copilot API). Use this along with `GITHUB_INFERENCE_TOKEN` to make LLM inference calls. |
94100
| `GITHUB_GIT_TOKEN` | Yes | Token used for authenticated `git clone` / `git push`. |
101+
| `GITHUB_SELECTED_MODEL` | No | Model selected by the platform for this run. Only set when model selection is enabled. |
102+
| `GITHUB_DEFAULT_MODEL` | No | Default model for the selected engine. Only set when model selection is enabled. |
103+
| `GITHUB_AVAILABLE_MODELS` | No | JSON array of models the engine can choose from (e.g. `["claude-sonnet-4.5","claude-opus-4.1"]`). Only set when model selection is enabled. |
104+
| `GITHUB_MODEL_VENDORS` | No | JSON array of model vendors as defined by the `vendors` field in `engine.yaml` (e.g. `["Anthropic","OpenAI"]`). Only set when model selection is enabled. |
95105

96106
## Step 2: Fetch Job Details
97107

@@ -141,6 +151,13 @@ Headers:
141151
"branch_name": "copilot/fix-123",
142152
"commit_login": "copilot-bot",
143153
"commit_email": "copilot-bot@users.noreply.github.com",
154+
"features": {
155+
"model_selection": true
156+
},
157+
"selected_model": "claude-sonnet-4.5",
158+
"default_model": "claude-sonnet-4.5",
159+
"available_models": ["claude-sonnet-4.5", "claude-opus-4.1"],
160+
"model_vendors": ["Anthropic"],
144161
"mcp_proxy_url": "http://127.0.0.1:2301"
145162
}
146163
```
@@ -155,6 +172,11 @@ Headers:
155172
| `branch_name` | Branch to checkout or create. |
156173
| `commit_login` | Git author name for commits. |
157174
| `commit_email` | Git author email for commits. |
175+
| `features` | Optional feature flags. Currently supports `model_selection` (boolean). |
176+
| `selected_model` | Model selected by the platform for this run. Present when `features.model_selection` is `true`. |
177+
| `default_model` | Default model for the selected engine. Present when `features.model_selection` is `true`. |
178+
| `available_models` | List of models the engine can choose from. Present when `features.model_selection` is `true`. |
179+
| `model_vendors` | List of model vendors as defined by the `vendors` field in `engine.yaml` (e.g. `["Anthropic", "OpenAI"]`). Present when `features.model_selection` is `true`. |
158180
| `mcp_proxy_url` | Optional URL of the MCP proxy server. When present, use it to discover user-provided MCP servers. See [User-Provided MCP Servers](#user-provided-mcp-servers). |
159181

160182
Use `GITHUB_INFERENCE_TOKEN` for model calls and `GITHUB_GIT_TOKEN` for git operations; those are bootstrap action inputs, not job response fields.
@@ -823,7 +845,7 @@ flowchart LR
823845
```
824846

825847
```typescript
826-
import { PlatformClient, cloneRepo, finalizeChanges } from "@github/copilot-engine-sdk";
848+
import { PlatformClient, cloneRepo, finalizeChanges, resolveSelectedModel } from "@github/copilot-engine-sdk";
827849
import { CopilotClient } from "@github/copilot-sdk";
828850

829851
async function main() {
@@ -860,6 +882,9 @@ async function main() {
860882

861883
// 5. Build system message based on action type
862884
const systemMessage = buildSystemMessage(job.action, job);
885+
const model = resolveSelectedModel(job, {
886+
fallbackModel: "claude-sonnet-4.5",
887+
}) ?? "claude-sonnet-4.5";
863888

864889
// 6. Run your agentic loop with your inference client
865890
const client = new CopilotClient({
@@ -870,7 +895,7 @@ async function main() {
870895
const mcpServerPath = require.resolve("@github/copilot-engine-sdk/mcp-server");
871896

872897
const session = await client.createSession({
873-
model: "claude-sonnet-4.5",
898+
model,
874899
systemMessage: { content: systemMessage },
875900
mcpServers: {
876901
"engine-tools": {

src/client.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,97 @@ export interface JobDetails {
485485
commit_login: string;
486486
commit_email: string;
487487
mcp_proxy_url?: string;
488+
/** Model selected by the platform for this run. Present when model selection is enabled. */
489+
selected_model?: string;
490+
/** Default model for the selected engine. Present when model selection is enabled. */
491+
default_model?: string;
492+
/** Models the engine can choose from. Present when model selection is enabled. */
493+
available_models?: string[];
494+
/** Model vendors for filtering (e.g. ["Anthropic", "OpenAI"]). Present when model selection is enabled. */
495+
model_vendors?: string[];
496+
/** Feature flags enabled for this job. */
497+
features?: {
498+
/** Whether the platform has enabled model selection for this job. */
499+
model_selection?: boolean;
500+
};
501+
}
502+
503+
/**
504+
* Check whether the platform has enabled model selection for this job.
505+
*
506+
* When this returns `false`, engines should use their own hardcoded model.
507+
*/
508+
export function isModelSelectionEnabled(job: Pick<JobDetails, "features">): boolean {
509+
return job.features?.model_selection === true;
510+
}
511+
512+
/**
513+
* Resolve which model an engine should use for a job.
514+
*
515+
* Returns `undefined` when model selection is not enabled for the job
516+
* (i.e. `features.model_selection` is not `true`), allowing engines that
517+
* do not support model selection to ignore it entirely.
518+
*
519+
* When enabled, the selection order is:
520+
* 1) caller preferred model
521+
* 2) model selected by platform (`selected_model`)
522+
* 3) platform-provided engine default (`default_model`)
523+
* 4) caller fallback model
524+
*
525+
* If `available_models` is present the resolved model must appear in that
526+
* list. When no candidate matches, the first available model is returned
527+
* and a warning is logged if `selected_model` was set but missing from the
528+
* list (indicates a platform misconfiguration).
529+
*/
530+
/** Options for {@link resolveSelectedModel}. */
531+
export interface ResolveSelectedModelOptions {
532+
/** Model the engine prefers to use, checked first. */
533+
preferredModel?: string;
534+
/** Model to fall back to when no platform-provided candidate matches. */
535+
fallbackModel?: string;
536+
}
537+
538+
export function resolveSelectedModel(
539+
job: Pick<JobDetails, "selected_model" | "default_model" | "available_models" | "features">,
540+
options?: ResolveSelectedModelOptions,
541+
): string | undefined {
542+
// Model selection must be explicitly enabled via feature flag
543+
if (job.features?.model_selection !== true) {
544+
return undefined;
545+
}
546+
547+
const availableModels = job.available_models
548+
?.map((model) => model.trim())
549+
.filter((model) => model.length > 0) ?? [];
550+
551+
const candidates = [
552+
options?.preferredModel,
553+
job.selected_model,
554+
job.default_model,
555+
options?.fallbackModel,
556+
].map((model) => model?.trim())
557+
.filter((model): model is string => Boolean(model && model.length > 0));
558+
559+
if (availableModels.length === 0) {
560+
return candidates[0];
561+
}
562+
563+
for (const candidate of candidates) {
564+
if (availableModels.includes(candidate)) {
565+
return candidate;
566+
}
567+
}
568+
569+
// Warn when the platform-selected model is not in the available list
570+
const trimmedSelectedModel = job.selected_model?.trim();
571+
if (trimmedSelectedModel && !availableModels.includes(trimmedSelectedModel)) {
572+
console.warn(
573+
`resolveSelectedModel: selected_model "${trimmedSelectedModel}" is not in available_models [${availableModels.join(", ")}]. ` +
574+
`Falling back to "${availableModels[0]}".`
575+
);
576+
}
577+
578+
return availableModels[0];
488579
}
489580

490581
/**

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,9 @@ export type {
9191
// Platform Client
9292
// =============================================================================
9393

94-
export { PlatformClient } from "./client.js";
94+
export { PlatformClient, resolveSelectedModel, isModelSelectionEnabled } from "./client.js";
9595

96-
export type { PlatformClientConfig, ProgressPayload, ProgressRecord, ProgressResponse, SendResult, JobDetails, ProblemStatement } from "./client.js";
96+
export type { PlatformClientConfig, ProgressPayload, ProgressRecord, ProgressResponse, SendResult, JobDetails, ProblemStatement, ResolveSelectedModelOptions } from "./client.js";
9797

9898
// =============================================================================
9999
// MCP Server

0 commit comments

Comments
 (0)