Skip to content
Merged
8 changes: 6 additions & 2 deletions .github/workflows/daily-cache-strategy-analyzer.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions .github/workflows/daily-caveman-optimizer.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions .github/workflows/daily-doc-healer.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .github/workflows/test-quality-sentinel.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions actions/setup/js/claude_harness.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const { emitMissingToolPermissionIssue, hasExpectedSafeOutputs, hasNoopInSafeOut
const { countPermissionDeniedIssues, hasNumerousPermissionDeniedIssues, extractDeniedCommands, buildMissingToolPermissionIssuePayload } = require("./permission_denied_helpers.cjs");
const { detectNonRetryableHarnessGuard, buildSoftTimeoutGuard, emitSoftTimeoutSignal, isAuthenticationFailedError } = require("./harness_retry_guard.cjs");
const { MODEL_NOT_SUPPORTED_PATTERN: INVALID_MODEL_ERROR_PATTERN } = require("./detect_agent_errors.cjs");
const { applyModelFallback } = require("./model_fallback.cjs");

// Pattern to detect Anthropic API overload errors (HTTP 529).
// Matches "overloaded_error" from the Anthropic error type field, and the
Expand Down Expand Up @@ -318,6 +319,7 @@ function stripContinueArgs(args) {
*/
async function buildClaudeChildEnv() {
const childEnv = { ...process.env };
applyModelFallback(childEnv, "ANTHROPIC_MODEL", log);
const provider = normalizeReflectProviderName(process.env.GH_AW_LLM_PROVIDER, "anthropic");
try {
const raw = fs.readFileSync(AWF_REFLECT_OUTPUT_PATH, "utf8");
Expand Down Expand Up @@ -611,6 +613,7 @@ if (typeof module !== "undefined" && module.exports) {
hasExpectedSafeOutputs,
resolveRetryConfig,
resolveStartupRetryLimit,
applyModelFallback,
};
}

Expand Down
18 changes: 18 additions & 0 deletions actions/setup/js/codex_harness.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const { countPermissionDeniedIssues, hasNumerousPermissionDeniedIssues, extractD
const { detectNonRetryableHarnessGuard, buildSoftTimeoutGuard, emitSoftTimeoutSignal, isAuthenticationFailedError } = require("./harness_retry_guard.cjs");
const { MODEL_NOT_SUPPORTED_PATTERN: INVALID_MODEL_ERROR_PATTERN } = require("./detect_agent_errors.cjs");
const { resolveRetryConfig } = require("./harness_retry_config.cjs");
const { applyModelFallback, injectModelFlagAfterExec } = require("./model_fallback.cjs");

// Pattern to detect OpenAI rate-limit errors.
// Matches the JSON error type field ("rate_limit_exceeded"), the HTTP status code
Expand Down Expand Up @@ -221,6 +222,16 @@ function injectJsonFlag(args) {
return ["exec", "--json", ...args.slice(1)];
}

function getCodexModelEnvVar(env = process.env) {
if ("GH_AW_MODEL_DETECTION_CODEX" in env) {
return "GH_AW_MODEL_DETECTION_CODEX";
}
if ("GH_AW_MODEL_AGENT_CODEX" in env) {
return "GH_AW_MODEL_AGENT_CODEX";
}
return "";
}

/**
* Build child process environment for Codex execution.
* Preserve API keys captured at harness startup, even if the parent environment
Expand Down Expand Up @@ -425,6 +436,10 @@ async function main() {
process.exit(1);
}

const codexModelEnvVar = getCodexModelEnvVar(process.env);
const resolvedModel = codexModelEnvVar ? applyModelFallback(process.env, codexModelEnvVar, log) : "";
resolvedArgs = injectModelFlagAfterExec(resolvedArgs, resolvedModel);

// Safe arg list for logging: when --prompt-file was present, the last element of
// resolvedArgs is the resolved prompt content. Replace it with a placeholder so that
// task instructions are never written to stderr or captured in agent logs.
Expand Down Expand Up @@ -641,6 +656,9 @@ if (typeof module !== "undefined" && module.exports) {
hasNoopInSafeOutputs,
hasExpectedSafeOutputs,
resolveRetryConfig,
applyModelFallback,
injectModelFlagAfterExec,
getCodexModelEnvVar,
};
}

Expand Down
3 changes: 3 additions & 0 deletions actions/setup/js/copilot_harness.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const { runSafeOutputsCLI, buildMissingToolAlternatives, emitMissingToolPermissi
const { countPermissionDeniedIssues, hasNumerousPermissionDeniedIssues, extractDeniedCommands, buildMissingToolPermissionIssuePayload } = require("./permission_denied_helpers.cjs");
const { detectNonRetryableHarnessGuard, buildSoftTimeoutGuard, emitSoftTimeoutSignal, isAuthenticationFailedError: isCommonAuthenticationFailedError } = require("./harness_retry_guard.cjs");
const { isCAPIQuotaExceededError } = require("./detect_agent_errors.cjs");
const { applyModelFallback } = require("./model_fallback.cjs");
const { loadModelsJson } = require("./model_costs.cjs");
const { resolveConfiguredCopilotModel } = require("./resolve_model_alias.cjs");

Expand Down Expand Up @@ -950,6 +951,7 @@ async function main() {
}
}

applyModelFallback(process.env, "COPILOT_MODEL", log);
applyCopilotModelAliasResolution({ awfReflectData, logger: log });
applyCopilotWireAPI({ modelsJson: loadModelsJson(), logger: log });

Expand Down Expand Up @@ -1456,6 +1458,7 @@ if (typeof module !== "undefined" && module.exports) {
parseCopilotSDKServerArgsFromEnv,
isCAPIQuotaExceededError,
hasTerminalSafeOutput,
applyModelFallback,
applyCopilotModelAliasResolution,
applyCopilotWireAPI,
loadAwfConfigData,
Expand Down
3 changes: 2 additions & 1 deletion actions/setup/js/copilot_sdk_driver.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const fs = require("fs");
const { runWithCopilotSDK, extractPromptFromArgs } = require("./copilot_sdk_session.cjs");
const { parsePermissionConfigFromServerArgs } = require("./copilot_sdk_permissions.cjs");
const { parseMultiProviderJson } = require("./copilot_sdk_multi_provider.cjs");
const { applyModelFallback } = require("./model_fallback.cjs");

// Re-export the session and permission helpers so that existing callers that
// require("./copilot_sdk_driver.cjs") (e.g. copilot_harness.cjs) continue to work.
Expand Down Expand Up @@ -103,7 +104,7 @@ async function main() {
const providers = multiProviderConfig.providers;
/** @type {import("@github/copilot-sdk").ProviderModelConfig[]} */
const sdkModels = multiProviderConfig.models;
let model = process.env.COPILOT_MODEL || multiProviderConfig.model || undefined;
let model = applyModelFallback(process.env, "COPILOT_MODEL", log) || multiProviderConfig.model || undefined;
log(`multi-provider mode: ${providers.length} providers, ${sdkModels.length} models, model=${model ?? "(env)"}`);
for (const p of providers) {
log(` provider: name=${p.name} type=${p.type} baseUrl=${p.baseUrl}${p.wireApi ? ` wireApi=${p.wireApi}` : ""}`);
Expand Down
3 changes: 2 additions & 1 deletion actions/setup/js/copilot_sdk_session.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const fs = require("fs");
const path = require("path");
const os = require("os");
const { buildCopilotSDKPermissionHandler, getEnvPositiveIntOrDefault, parseMaxToolDenialsLimit, MAX_TOOL_DENIALS_DEFAULT } = require("./copilot_sdk_permissions.cjs");
const { resolveModelWithFallback } = require("./model_fallback.cjs");
const { extractShellCommandFromToolData } = require("./tool_call_details.cjs");

// Default timeout for a single sendAndWait call: 10 minutes.
Expand Down Expand Up @@ -254,7 +255,7 @@ async function runWithCopilotSDK({ sdkUri, prompt, logger, attempt = 0, model, c
// Build session config using the multi-provider surface.
/** @type {import("@github/copilot-sdk").SessionConfig} */
const sessionConfig = {
model: model || process.env.COPILOT_MODEL || undefined,
model: model || resolveModelWithFallback(process.env, "COPILOT_MODEL") || undefined,
providers,
models: providerModels,
onPermissionRequest,
Expand Down
48 changes: 48 additions & 0 deletions actions/setup/js/model_fallback.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use strict";

const MODEL_FALLBACK_ENV_VAR = "GH_AW_MODEL_FALLBACK";

function readTrimmedEnv(env, name) {
return typeof env?.[name] === "string" ? env[name].trim() : "";
}

function resolveModelWithFallback(env, primaryEnvVar) {
return readTrimmedEnv(env, primaryEnvVar) || readTrimmedEnv(env, MODEL_FALLBACK_ENV_VAR);
}

/**
* @param {NodeJS.ProcessEnv} env
* @param {string} primaryEnvVar
* @param {(message: string) => void} [logger]
* @returns {string}
*/
function applyModelFallback(env, primaryEnvVar, logger = () => {}) {
const primary = readTrimmedEnv(env, primaryEnvVar);
if (primary) {
return primary;
}
const fallback = readTrimmedEnv(env, MODEL_FALLBACK_ENV_VAR);
if (fallback) {
env[primaryEnvVar] = fallback;
logger(`applied ${MODEL_FALLBACK_ENV_VAR} to ${primaryEnvVar}`);
}
return fallback;
}

function injectModelFlagAfterExec(args, model) {
if (!model || args.includes("--model")) {
return args;
}
const execIndex = args.indexOf("exec");
if (execIndex === -1) {
return [...args, "--model", model];
}
return [...args.slice(0, execIndex + 1), "--model", model, ...args.slice(execIndex + 1)];
}

module.exports = {
MODEL_FALLBACK_ENV_VAR,
resolveModelWithFallback,
applyModelFallback,
injectModelFlagAfterExec,
};
25 changes: 25 additions & 0 deletions actions/setup/js/model_fallback.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";
import { createRequire } from "module";

const require = createRequire(import.meta.url);
const { MODEL_FALLBACK_ENV_VAR, resolveModelWithFallback, applyModelFallback, injectModelFlagAfterExec } = require("./model_fallback.cjs");

describe("model_fallback.cjs", () => {
it("prefers the primary model env var when it is non-empty", () => {
expect(resolveModelWithFallback({ COPILOT_MODEL: "gpt-5", [MODEL_FALLBACK_ENV_VAR]: "claude-sonnet-4.6" }, "COPILOT_MODEL")).toBe("gpt-5");
});

it("uses the fallback model when the primary resolves empty", () => {
const env = { COPILOT_MODEL: "", [MODEL_FALLBACK_ENV_VAR]: "claude-sonnet-4.6" };
expect(applyModelFallback(env, "COPILOT_MODEL")).toBe("claude-sonnet-4.6");
expect(env.COPILOT_MODEL).toBe("claude-sonnet-4.6");
});

it("injects --model immediately after exec when needed", () => {
expect(injectModelFlagAfterExec(["exec", "--json", "prompt"], "gpt-5")).toEqual(["exec", "--model", "gpt-5", "--json", "prompt"]);
});

it("does not inject --model when already present", () => {
expect(injectModelFlagAfterExec(["exec", "--model", "gpt-5", "--json"], "claude-sonnet-4.6")).toEqual(["exec", "--model", "gpt-5", "--json"]);
});
});
3 changes: 2 additions & 1 deletion actions/setup/js/run_evals.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const path = require("path");

const { ERR_VALIDATION } = require("./error_codes.cjs");
const { EVALS_OUTPUT_PATH } = require("./evals_constants.cjs");
const { resolveModelWithFallback } = require("./model_fallback.cjs");

const EVALS_DIR = "/tmp/gh-aw/evals";
const EVALS_LOG_PATH = "/tmp/gh-aw/evals/evals.log";
Expand Down Expand Up @@ -118,7 +119,7 @@ async function setupMain() {
*/
async function parseMain() {
const questionsRaw = process.env.GH_AW_EVALS_QUESTIONS;
const model = process.env.GH_AW_EVALS_MODEL || "";
const model = resolveModelWithFallback(process.env, "GH_AW_EVALS_MODEL") || "";
const runID = process.env.GITHUB_RUN_ID || "unknown";

/** @type {Array<{id: string, question: string}>} */
Expand Down
14 changes: 14 additions & 0 deletions actions/setup/js/run_evals.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const EVALS_DIR = "/tmp/gh-aw/evals";
const EVALS_LOG_PATH = `${EVALS_DIR}/evals.log`;
const EVALS_OUTPUT_PATH = "/tmp/gh-aw/evals.jsonl";
const require = createRequire(import.meta.url);
const { MODEL_FALLBACK_ENV_VAR } = require("./model_fallback.cjs");
const { parseMain, extractAssistantTextFromJsonlLog } = require("./run_evals.cjs");

const mockCore = {
Expand Down Expand Up @@ -75,6 +76,19 @@ describe("run_evals.cjs", () => {
expect(JSON.parse(line).runid).toBe("unknown");
});

it("uses GH_AW_MODEL_FALLBACK when GH_AW_EVALS_MODEL resolves empty", async () => {
vi.stubEnv("GH_AW_EVALS_QUESTIONS", JSON.stringify([{ id: "labels-applied", question: "Did labels get applied?" }]));
vi.stubEnv("GH_AW_EVALS_MODEL", "");
vi.stubEnv(MODEL_FALLBACK_ENV_VAR, "claude-sonnet-4.6");
vi.stubEnv("GITHUB_RUN_ID", "123456789");
fs.writeFileSync(EVALS_LOG_PATH, "labels-applied: YES\n", "utf8");

await parseMain();

const [line] = fs.readFileSync(EVALS_OUTPUT_PATH, "utf8").trim().split("\n");
expect(JSON.parse(line).model).toBe("claude-sonnet-4.6");
});

it("parses answers from Pi v3 JSONL turn_end events (positional format)", async () => {
vi.stubEnv(
"GH_AW_EVALS_QUESTIONS",
Expand Down
5 changes: 5 additions & 0 deletions pkg/constants/engine_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,11 @@ const (
// EnvVarModelAgentPi configures the default Pi model for agent execution
EnvVarModelAgentPi = "GH_AW_MODEL_AGENT_PI"

// EnvVarModelFallback carries the standard org/enterprise/built-in model fallback
// expression for the current engine step. Runtime JavaScript harnesses can use this
// when a configured model expression resolves to an empty string.
EnvVarModelFallback = "GH_AW_MODEL_FALLBACK"

// CopilotCLIModelEnvVar is the native environment variable name supported by the Copilot CLI
// for selecting the model. Setting this env var is equivalent to passing --model to the CLI.
CopilotCLIModelEnvVar = "COPILOT_MODEL"
Expand Down
Loading
Loading