Skip to content

Commit c694930

Browse files
Improvements:
1 parent b9ca4b7 commit c694930

File tree

6 files changed

+202
-26
lines changed

6 files changed

+202
-26
lines changed

cli/cmd/engine-cli/main.go

Lines changed: 26 additions & 7 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+
selectedEngine string
38+
selectedModel string
39+
defaultModel string
40+
availableModels []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(&selectedEngine, "selected-engine", "", "Selected engine family for this job (for example: claude or codex)")
98+
runCmd.Flags().StringVar(&selectedModel, "selected-model", "", "Selected model for this job")
99+
runCmd.Flags().StringVar(&defaultModel, "default-model", "", "Default model for this engine")
100+
runCmd.Flags().StringSliceVar(&availableModels, "available-model", nil, "Available model for this engine (repeatable)")
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+
SelectedEngine: selectedEngine,
164+
SelectedModel: selectedModel,
165+
DefaultModel: defaultModel,
166+
AvailableModels: availableModels,
152167
}
153168

154169
prNumber := setup.PRNumber
@@ -258,13 +273,17 @@ func runEngine(cmd *cobra.Command, args []string) error {
258273
}
259274

260275
env := runner.Environment{
261-
JobID: jobID,
262-
APIToken: githubToken,
263-
APIURL: apiURL,
264-
JobNonce: mockServer.Nonce(),
265-
InferenceToken: githubToken,
266-
InferenceURL: inferenceURL,
267-
GitToken: githubToken,
276+
JobID: jobID,
277+
APIToken: githubToken,
278+
APIURL: apiURL,
279+
JobNonce: mockServer.Nonce(),
280+
InferenceToken: githubToken,
281+
InferenceURL: inferenceURL,
282+
GitToken: githubToken,
283+
SelectedEngine: selectedEngine,
284+
SelectedModel: selectedModel,
285+
DefaultModel: defaultModel,
286+
AvailableModels: availableModels,
268287
}
269288

270289
result := runner.Run(ctx, command, env, runner.Options{WorkingDir: workingDir}, runnerCallbacks)

cli/internal/runner/runner.go

Lines changed: 37 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+
SelectedEngine string
27+
SelectedModel string
28+
DefaultModel string
29+
AvailableModels []string
2530
}
2631

2732
// Callbacks contains optional callbacks for runner events.
@@ -124,18 +129,37 @@ 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.SelectedEngine != "" {
145+
platformVars["GITHUB_SELECTED_ENGINE"] = env.SelectedEngine
146+
}
147+
148+
if env.SelectedModel != "" {
149+
platformVars["GITHUB_SELECTED_MODEL"] = env.SelectedModel
150+
}
151+
152+
if env.DefaultModel != "" {
153+
platformVars["GITHUB_DEFAULT_MODEL"] = env.DefaultModel
154+
}
155+
156+
if len(env.AvailableModels) > 0 {
157+
// json.Marshal cannot fail for []string, but handle the error defensively.
158+
if encoded, err := json.Marshal(env.AvailableModels); err == nil {
159+
platformVars["GITHUB_AVAILABLE_MODELS"] = string(encoded)
160+
}
161+
}
162+
139163
for k, v := range platformVars {
140164
result = append(result, fmt.Sprintf("%s=%s", k, v))
141165
}

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+
SelectedEngine string
36+
SelectedModel string
37+
DefaultModel string
38+
AvailableModels []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.SelectedEngine != "" {
220+
response["selected_engine"] = s.jobConfig.SelectedEngine
221+
}
222+
223+
if s.jobConfig.SelectedModel != "" {
224+
response["selected_model"] = s.jobConfig.SelectedModel
225+
}
226+
227+
if s.jobConfig.DefaultModel != "" {
228+
response["default_model"] = s.jobConfig.DefaultModel
229+
}
230+
231+
if len(s.jobConfig.AvailableModels) > 0 {
232+
response["available_models"] = s.jobConfig.AvailableModels
233+
}
234+
}
235+
209236
if s.callbacks.OnJobFetched != nil {
210237
s.callbacks.OnJobFetched()
211238
}

docs/integration-guide.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ The platform injects these environment variables into the engine process at runt
9494
| `GITHUB_INFERENCE_TOKEN` | Yes | Token used by your inference client / SDK for model calls. |
9595
| `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. |
9696
| `GITHUB_GIT_TOKEN` | Yes | Token used for authenticated `git clone` / `git push`. |
97+
| `GITHUB_SELECTED_ENGINE` | No | Engine family selected for this run (e.g. `claude`, `codex`). Only set when model selection is enabled. |
98+
| `GITHUB_SELECTED_MODEL` | No | Model selected by the platform for this run. Only set when model selection is enabled. |
99+
| `GITHUB_DEFAULT_MODEL` | No | Default model for the selected engine. Only set when model selection is enabled. |
100+
| `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. |
97101

98102
## Step 2: Fetch Job Details
99103

@@ -143,6 +147,13 @@ Headers:
143147
"branch_name": "copilot/fix-123",
144148
"commit_login": "copilot-bot",
145149
"commit_email": "copilot-bot@users.noreply.github.com",
150+
"features": {
151+
"model_selection": true
152+
},
153+
"selected_engine": "claude",
154+
"selected_model": "claude-sonnet-4.5",
155+
"default_model": "claude-sonnet-4.5",
156+
"available_models": ["claude-sonnet-4.5", "claude-opus-4.1"],
146157
"mcp_proxy_url": "http://127.0.0.1:2301"
147158
}
148159
```
@@ -157,6 +168,11 @@ Headers:
157168
| `branch_name` | Branch to checkout or create. |
158169
| `commit_login` | Git author name for commits. |
159170
| `commit_email` | Git author email for commits. |
171+
| `features` | Optional feature flags. Currently supports `model_selection` (boolean). |
172+
| `selected_engine` | Engine family selected for this run (e.g. `claude` or `codex`). Present when `features.model_selection` is `true`. |
173+
| `selected_model` | Model selected by the platform for this run. Present when `features.model_selection` is `true`. |
174+
| `default_model` | Default model for the selected engine. Present when `features.model_selection` is `true`. |
175+
| `available_models` | List of models the engine can choose from. Present when `features.model_selection` is `true`. |
160176
| `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). |
161177

162178
Use `GITHUB_INFERENCE_TOKEN` for model calls and `GITHUB_GIT_TOKEN` for git operations; those are bootstrap action inputs, not job response fields.
@@ -825,7 +841,7 @@ flowchart LR
825841
```
826842

827843
```typescript
828-
import { PlatformClient, cloneRepo, finalizeChanges } from "@github/copilot-engine-sdk";
844+
import { PlatformClient, cloneRepo, finalizeChanges, resolveSelectedModel } from "@github/copilot-engine-sdk";
829845
import { CopilotClient } from "@github/copilot-sdk";
830846

831847
async function main() {
@@ -862,6 +878,9 @@ async function main() {
862878

863879
// 5. Build system message based on action type
864880
const systemMessage = buildSystemMessage(job.action, job);
881+
const model = resolveSelectedModel(job, {
882+
fallbackModel: "claude-sonnet-4.5",
883+
}) ?? "claude-sonnet-4.5";
865884

866885
// 6. Run your agentic loop with your inference client
867886
const client = new CopilotClient({
@@ -872,7 +891,7 @@ async function main() {
872891
const mcpServerPath = require.resolve("@github/copilot-engine-sdk/mcp-server");
873892

874893
const session = await client.createSession({
875-
model: "claude-sonnet-4.5",
894+
model,
876895
systemMessage: { content: systemMessage },
877896
mcpServers: {
878897
"engine-tools": {

src/client.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,93 @@ export interface JobDetails {
485485
commit_login: string;
486486
commit_email: string;
487487
mcp_proxy_url?: string;
488+
/** Engine family selected for this run (e.g., "claude" or "codex"). Present when model selection is enabled. */
489+
selected_engine?: string;
490+
/** Model selected by the platform for this run. Present when model selection is enabled. */
491+
selected_model?: string;
492+
/** Default model for the selected engine. Present when model selection is enabled. */
493+
default_model?: string;
494+
/** Models the engine can choose from. Present when model selection is enabled. */
495+
available_models?: 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) {
544+
return undefined;
545+
}
546+
547+
const availableModels = job.available_models?.filter((model) => model.trim().length > 0) ?? [];
548+
549+
const candidates = [
550+
options?.preferredModel,
551+
job.selected_model,
552+
job.default_model,
553+
options?.fallbackModel,
554+
].filter((model): model is string => Boolean(model && model.trim().length > 0));
555+
556+
if (availableModels.length === 0) {
557+
return candidates[0];
558+
}
559+
560+
for (const candidate of candidates) {
561+
if (availableModels.includes(candidate)) {
562+
return candidate;
563+
}
564+
}
565+
566+
// Warn when the platform-selected model is not in the available list
567+
if (job.selected_model && !availableModels.includes(job.selected_model)) {
568+
console.warn(
569+
`resolveSelectedModel: selected_model "${job.selected_model}" is not in available_models [${availableModels.join(", ")}]. ` +
570+
`Falling back to "${availableModels[0]}".`
571+
);
572+
}
573+
574+
return availableModels[0];
488575
}
489576

490577
/**

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)