Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix Claude endpoint routing attribution
Co-authored-by: SivaKesava1 <11771739+SivaKesava1@users.noreply.github.com>
  • Loading branch information
Copilot and SivaKesava1 authored Oct 8, 2026
commit 218ca3f90a5d6d1e3fedd2ba0871ccec6f9784e8
25 changes: 24 additions & 1 deletion actions/setup/js/awf_model_routing.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ const fs = require("fs");
const path = require("path");

const ROUTING_REASONING_EFFORTS = Object.freeze(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
const ROUTING_ENDPOINTS = Object.freeze(["/v1/messages", "/responses", "/chat/completions"]);
const AWF_MODEL_ROUTING_POLICIES = Object.freeze({
copilot: Object.freeze({ endpoints: Object.freeze(["/responses", "/chat/completions"]), allowEndpointOverride: false }),
claude: Object.freeze({ endpoints: Object.freeze(["/v1/messages"]), allowEndpointOverride: true }),
codex: Object.freeze({ endpoints: Object.freeze(["/responses"]), allowEndpointOverride: true }),
pi: Object.freeze({ endpoints: ROUTING_ENDPOINTS, allowEndpointOverride: true }),
});
const EMPTY_MODEL_ROUTING_POLICY = Object.freeze({ endpoints: Object.freeze([]), allowEndpointOverride: false });

/**
* @param {string} engine
* @returns {{endpoints: string[], allowEndpointOverride: boolean}}
*/
function getAWFModelRoutingPolicy(engine) {
return AWF_MODEL_ROUTING_POLICIES[String(engine || "").toLowerCase()] || EMPTY_MODEL_ROUTING_POLICY;
}

/**
* @param {unknown} effort
Expand Down Expand Up @@ -73,7 +89,7 @@ function resolveAWFModelRoutingSelection(reflectData, routingRequired = false, a
if (routingModelAmbiguity) return { selection: null, error: routingModelAmbiguity };
let effectiveEndpoint = endpoint;
if (allowedEndpoints && !allowedEndpoints.includes(endpoint)) {
if (!allowEndpointOverride) {
if (!allowEndpointOverride || !ROUTING_ENDPOINTS.includes(endpoint)) {
return { selection: null, error: `AWF /reflect selected endpoint ${endpoint}, which is not supported by this engine` };
}
const routingModel = getAWFRoutingModel(reflectData, wireModel);
Expand Down Expand Up @@ -142,13 +158,17 @@ function recordAWFModelRoutingOutcome(outcome, env = process.env, filePath = pat
if (env.GH_AW_MODEL_ROUTING !== "1" || !outcome || !["selected", "failed", "rejected"].includes(outcome.status)) return false;
const wireModel = typeof outcome.wire_model === "string" && outcome.wire_model.length <= 128 && /^[A-Za-z0-9._/:@-]+$/.test(outcome.wire_model) ? outcome.wire_model : "";
if (outcome.status === "selected" && !wireModel) return false;
const endpoint = ROUTING_ENDPOINTS.includes(outcome.endpoint) ? outcome.endpoint : "";
const selectedEndpoint = ROUTING_ENDPOINTS.includes(outcome.selected_endpoint) ? outcome.selected_endpoint : "";
const effort = outcome.effort == null ? null : isRoutingReasoningEffort(outcome.effort) ? outcome.effort : null;
if (outcome.effort != null && effort === null) return false;
const appliedEffort = outcome.applied_effort == null ? null : outcome.applied_effort === "off" || isRoutingReasoningEffort(outcome.applied_effort) ? outcome.applied_effort : null;
if (outcome.applied_effort != null && appliedEffort === null) return false;
const record = {
status: outcome.status,
...(wireModel ? { wire_model: wireModel } : {}),
...(endpoint ? { endpoint } : {}),
...(selectedEndpoint ? { selected_endpoint: selectedEndpoint } : {}),
...(effort ? { effort } : {}),
...(appliedEffort ? { applied_effort: appliedEffort } : {}),
...(typeof outcome.failure_code === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(outcome.failure_code) ? { failure_code: outcome.failure_code } : {}),
Expand Down Expand Up @@ -181,6 +201,9 @@ function isModelAvailableInReflectData(model, reflectData, allowedProviders = nu

module.exports = {
ROUTING_REASONING_EFFORTS,
ROUTING_ENDPOINTS,
AWF_MODEL_ROUTING_POLICIES,
getAWFModelRoutingPolicy,
isRoutingReasoningEffort,
mapAWFRoutingEffort,
resolveAWFModelRoutingSelection,
Expand Down
19 changes: 18 additions & 1 deletion actions/setup/js/awf_model_routing.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,30 @@ describe("awf_model_routing.cjs", () => {
const env = { GH_AW_MODEL_ROUTING: "1", GH_AW_TMP_DIR: tmpDir };
const recordPath = path.join(tmpDir, "agent", "awf-routing-outcome.json");
try {
expect(recordAWFModelRoutingOutcome({ status: "selected", wire_model: "gpt-5.6-luna", effort: "high", applied_effort: "high" }, env)).toBe(true);
expect(
recordAWFModelRoutingOutcome(
{
status: "selected",
wire_model: "gpt-5.6-luna",
endpoint: "/responses",
selected_endpoint: "/chat/completions",
effort: "high",
applied_effort: "high",
},
env
)
).toBe(true);
expect(JSON.parse(fs.readFileSync(recordPath, "utf8"))).toMatchObject({
status: "selected",
wire_model: "gpt-5.6-luna",
endpoint: "/responses",
selected_endpoint: "/chat/completions",
effort: "high",
applied_effort: "high",
});
expect(recordAWFModelRoutingOutcome({ status: "selected", wire_model: "gpt-5.6-luna", endpoint: "/invalid", selected_endpoint: "\n" }, env)).toBe(true);
expect(JSON.parse(fs.readFileSync(recordPath, "utf8"))).not.toHaveProperty("endpoint");
expect(JSON.parse(fs.readFileSync(recordPath, "utf8"))).not.toHaveProperty("selected_endpoint");
expect(recordAWFModelRoutingOutcome({ status: "rejected", failure_code: "unsupported_effort" }, env)).toBe(true);
expect(JSON.parse(fs.readFileSync(recordPath, "utf8"))).toMatchObject({ status: "rejected", failure_code: "unsupported_effort" });
expect(recordAWFModelRoutingOutcome({ status: "selected", wire_model: "gpt-5.6-luna\nrouted: false" }, env)).toBe(false);
Expand Down
21 changes: 15 additions & 6 deletions actions/setup/js/claude_harness.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,8 @@ const { isCrashSignalExitCode, crashSignalNameForExitCode } = require("./harness
const { MODEL_NOT_SUPPORTED_PATTERN: INVALID_MODEL_ERROR_PATTERN } = require("./detect_agent_errors.cjs");
const { applyModelFallback, normalizeClaudeModel, normalizeClaudeModelArgs } = require("./model_fallback.cjs");
const { parseMaxAICreditsExceededFromAuditLog } = require("./ai_credits_context.cjs");
const { resolveAWFModelRoutingSelection, mapAWFRoutingEffort, recordAWFModelRoutingOutcome } = require("./awf_model_routing.cjs");

const CLAUDE_ROUTING_ENDPOINTS = ["/v1/messages"];
const { resolveAWFModelRoutingSelection, mapAWFRoutingEffort, recordAWFModelRoutingOutcome, getAWFModelRoutingPolicy } = require("./awf_model_routing.cjs");
const CLAUDE_ROUTING_POLICY = getAWFModelRoutingPolicy("claude");

// Pattern to detect Anthropic API overload errors (HTTP 529).
// Matches "overloaded_error" from the Anthropic error type field, and the
Expand Down Expand Up @@ -377,7 +376,7 @@ async function buildClaudeChildEnv(reflectData, env = process.env, logger = log,
const childEnv = { ...env };
applyClaudeRuntimeTimeouts(childEnv);
if (env.GH_AW_MODEL_ROUTING === "1") {
const result = modelRoutingSelection ? { selection: modelRoutingSelection, error: null } : resolveAWFModelRoutingSelection(reflectData, true, CLAUDE_ROUTING_ENDPOINTS, true);
const result = modelRoutingSelection ? { selection: modelRoutingSelection, error: null } : resolveAWFModelRoutingSelection(reflectData, true, CLAUDE_ROUTING_POLICY.endpoints, CLAUDE_ROUTING_POLICY.allowEndpointOverride);
if (result.error || !result.selection) {
recordAWFModelRoutingOutcome({ status: reflectData?.routing?.status === "failed" ? "failed" : "rejected", failure_code: reflectData?.routing?.failure_code, detail: result.error });
throw new Error(`${result.error || "AWF model routing selection is missing"}; refusing to start Claude`);
Expand All @@ -391,7 +390,17 @@ async function buildClaudeChildEnv(reflectData, env = process.env, logger = log,
delete childEnv.GH_AW_MODEL_FALLBACK;
if (mappedEffort.effort) childEnv.CLAUDE_CODE_EFFORT_LEVEL = mappedEffort.effort;
else delete childEnv.CLAUDE_CODE_EFFORT_LEVEL;
recordAWFModelRoutingOutcome({ status: "selected", wire_model: result.selection.wire_model, effort: result.selection.effort, applied_effort: mappedEffort.effort }, env);
recordAWFModelRoutingOutcome(
{
status: "selected",
wire_model: result.selection.wire_model,
endpoint: result.selection.endpoint,
selected_endpoint: result.selection.selected_endpoint,
effort: result.selection.effort,
applied_effort: mappedEffort.effort,
},
env
);
const endpointOverride = result.selection.selected_endpoint && result.selection.selected_endpoint !== result.selection.endpoint ? ` selected_endpoint=${result.selection.selected_endpoint}` : "";
logger(`inference routing: mode=awf-routed model=${result.selection.wire_model} effort=${result.selection.effort || "(unset)"} endpoint=${result.selection.endpoint}${endpointOverride}`);
} else {
Expand Down Expand Up @@ -470,7 +479,7 @@ async function main() {
/** @type {any} */
let modelRoutingSelection = null;
if (process.env.GH_AW_MODEL_ROUTING === "1") {
const result = resolveAWFModelRoutingSelection(reflection.reflectData, true, CLAUDE_ROUTING_ENDPOINTS, true);
const result = resolveAWFModelRoutingSelection(reflection.reflectData, true, CLAUDE_ROUTING_POLICY.endpoints, CLAUDE_ROUTING_POLICY.allowEndpointOverride);
if (result.error || !result.selection) {
recordAWFModelRoutingOutcome({ status: reflection.reflectData?.routing?.status === "failed" ? "failed" : "rejected", failure_code: reflection.reflectData?.routing?.failure_code, detail: result.error });
throw new Error(`${result.error || "AWF model routing selection is missing"}; refusing to start Claude`);
Expand Down
15 changes: 12 additions & 3 deletions actions/setup/js/claude_harness.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,18 @@ describe("claude_harness.cjs", () => {
routing: { ...routedReflect.routing, selection: { ...routedReflect.routing.selection, effort: "max", endpoint: "/chat/completions" } },
};
const messages = [];
const child = await buildClaudeChildEnv(reflect, { GH_AW_MODEL_ROUTING: "1", GH_AW_LLM_PROVIDER: "github" }, message => messages.push(message));
expect(child).toMatchObject({ ANTHROPIC_MODEL: "claude-opus-5", CLAUDE_CODE_EFFORT_LEVEL: "max" });
expect(messages[0]).toContain("endpoint=/v1/messages selected_endpoint=/chat/completions");
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "claude-routing-outcome-"));
try {
const child = await buildClaudeChildEnv(reflect, { GH_AW_MODEL_ROUTING: "1", GH_AW_LLM_PROVIDER: "github", GH_AW_TMP_DIR: tmpDir }, message => messages.push(message));
expect(child).toMatchObject({ ANTHROPIC_MODEL: "claude-opus-5", CLAUDE_CODE_EFFORT_LEVEL: "max" });
expect(messages[0]).toContain("endpoint=/v1/messages selected_endpoint=/chat/completions");
expect(JSON.parse(fs.readFileSync(path.join(tmpDir, "agent", "awf-routing-outcome.json"), "utf8"))).toMatchObject({
endpoint: "/v1/messages",
selected_endpoint: "/chat/completions",
});
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});

it("starts the harness with the routed model and refuses an endpoint mismatch", () => {
Expand Down
16 changes: 11 additions & 5 deletions actions/setup/js/codex_harness.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,8 @@ const { resolveRetryConfig } = require("./harness_retry_config.cjs");
const { applyModelFallback, injectModelFlagAfterExec, normalizeCodexModel, normalizeCodexModelArgs } = require("./model_fallback.cjs");
const { parseMaxAICreditsExceededFromAuditLog } = require("./ai_credits_context.cjs");
const { calculateWorkingSetFromJSONL } = require("./working_set_metrics.cjs");
const { resolveAWFModelRoutingSelection, mapAWFRoutingEffort, recordAWFModelRoutingOutcome } = require("./awf_model_routing.cjs");

const CODEX_ROUTING_ENDPOINTS = ["/responses"];
const { resolveAWFModelRoutingSelection, mapAWFRoutingEffort, recordAWFModelRoutingOutcome, getAWFModelRoutingPolicy } = require("./awf_model_routing.cjs");
const CODEX_ROUTING_POLICY = getAWFModelRoutingPolicy("codex");

// Pattern to detect OpenAI rate-limit errors.
// Matches the JSON error type field ("rate_limit_exceeded"), the HTTP status code
Expand Down Expand Up @@ -514,7 +513,7 @@ function applyCodexRoutingEffort(args, effort) {
* @returns {{selection: any, model: string, args: string[], error: string|null}}
*/
function resolveCodexModelRouting(reflectData, args) {
const result = resolveAWFModelRoutingSelection(reflectData, true, CODEX_ROUTING_ENDPOINTS, true);
const result = resolveAWFModelRoutingSelection(reflectData, true, CODEX_ROUTING_POLICY.endpoints, CODEX_ROUTING_POLICY.allowEndpointOverride);
if (result.error || !result.selection) {
recordAWFModelRoutingOutcome({ status: reflectData?.routing?.status === "failed" ? "failed" : "rejected", failure_code: reflectData?.routing?.failure_code, detail: result.error });
return { selection: null, model: "", args, error: result.error || "AWF model routing selection is missing" };
Expand All @@ -527,7 +526,14 @@ function resolveCodexModelRouting(reflectData, args) {
let routedArgs = removeCodexRoutingOverrides(args);
routedArgs = injectModelFlagAfterExec(routedArgs, result.selection.wire_model);
if (mappedEffort.effort) routedArgs = applyCodexRoutingEffort(routedArgs, mappedEffort.effort);
recordAWFModelRoutingOutcome({ status: "selected", wire_model: result.selection.wire_model, effort: result.selection.effort, applied_effort: mappedEffort.effort });
recordAWFModelRoutingOutcome({
status: "selected",
wire_model: result.selection.wire_model,
endpoint: result.selection.endpoint,
selected_endpoint: result.selection.selected_endpoint,
effort: result.selection.effort,
applied_effort: mappedEffort.effort,
});
return { selection: result.selection, model: result.selection.wire_model, args: routedArgs, error: null };
}

Expand Down
5 changes: 3 additions & 2 deletions actions/setup/js/copilot_harness.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ const { isCrashSignalExitCode, crashSignalNameForExitCode } = require("./harness
const { isCAPIQuotaExceededError, isCAPIServerError } = require("./detect_agent_errors.cjs");
const { applyModelFallback } = require("./model_fallback.cjs");
const { isRoutingReasoningEffort } = require("./copilot_routing_effort.cjs");
const { resolveAWFModelRoutingSelection, isModelAvailableInReflectData } = require("./awf_model_routing.cjs");
const { resolveAWFModelRoutingSelection, isModelAvailableInReflectData, getAWFModelRoutingPolicy } = require("./awf_model_routing.cjs");
const { recordAWFModelRoutingOutcome } = require("./awf_model_routing.cjs");
const COPILOT_ROUTING_POLICY = getAWFModelRoutingPolicy("copilot");
const { loadModelsJson } = require("./model_costs.cjs");
const { resolveConfiguredCopilotModel, ModelAliasResolutionError } = require("./resolve_model_alias.cjs");
const { parseAICreditsErrorInfoFromAuditLog, parseMaxAICreditsFromAuditLog, parseMaxAICreditsExceededFromAuditLog, parseAPIProxyGuardRejectionFromEventLog, formatAPIProxyGuardRejection } = require("./ai_credits_context.cjs");
Expand Down Expand Up @@ -1187,7 +1188,7 @@ async function main() {
}
}

const routingResult = resolveAWFModelRoutingSelection(awfReflectData, modelRoutingRequired, ["/responses", "/chat/completions"]);
const routingResult = resolveAWFModelRoutingSelection(awfReflectData, modelRoutingRequired, COPILOT_ROUTING_POLICY.endpoints, COPILOT_ROUTING_POLICY.allowEndpointOverride);
if (routingResult.error) {
recordAWFModelRoutingOutcome({ status: awfReflectData?.routing?.status === "failed" ? "failed" : "rejected", failure_code: awfReflectData?.routing?.failure_code, detail: routingResult.error });
log(`unexpected error: AWF model routing failed: ${routingResult.error}; refusing to start Copilot`);
Expand Down
1 change: 1 addition & 0 deletions actions/setup/js/model_attribution.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ function recordModelRouting(routing, env = process.env, infoPath = `${env.GH_AW_
...(effort ? { effort } : {}),
...(appliedEffort ? { applied_effort: appliedEffort } : {}),
...(routing.endpoint ? { endpoint: validateModelIdentifier(routing.endpoint) } : {}),
...(routing.selected_endpoint ? { selected_endpoint: validateModelIdentifier(routing.selected_endpoint) } : {}),
...(routing.mode ? { mode: validateModelIdentifier(routing.mode) } : {}),
...(routing.selected_id ? { selected_id: validateModelIdentifier(routing.selected_id) } : {}),
...(routing.router_version ? { router_version: validateModelIdentifier(routing.router_version) } : {}),
Expand Down
Loading
Loading