Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 4 additions & 4 deletions actions/setup/js/send_otlp_span.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1929,7 +1929,7 @@ async function sendJobConclusionSpan(spanName, options = {}) {
const bodyModified = typeof awInfo.body_modified === "boolean" ? awInfo.body_modified : parseBooleanEnv(process.env.GH_AW_INFO_BODY_MODIFIED);
const trackerId = process.env.GH_AW_TRACKER_ID || awInfo.tracker_id || "";
const jobName = process.env.INPUT_JOB_NAME || "";
const jobEmitsOwnTokenUsage = jobName === "agent" || jobName === "detection";
const jobEmitsOwnTokenUsage = jobName === "agent" || jobName === "detection" || (!!engineId && jobName === engineId);
const runId = process.env.GITHUB_RUN_ID || "";
const runAttempt = awInfo.run_attempt || process.env.GITHUB_RUN_ATTEMPT || "1";
const actor = process.env.GITHUB_ACTOR || "";
Expand Down Expand Up @@ -2052,8 +2052,8 @@ async function sendJobConclusionSpan(spanName, options = {}) {
if (frontmatterEmoji) attributes.push(buildAttr("gh-aw.frontmatter.emoji", frontmatterEmoji));
if (typeof bodyModified === "boolean") attributes.push(buildAttr("gh-aw.frontmatter.body_modified", bodyModified));
attributes.push(...buildEpisodeAttributesFromContext(awInfo, runId, runAttempt));
// GH_AW_AIC is propagated to downstream jobs via needs.agent.outputs.*, so gate it
// behind jobEmitsOwnTokenUsage to prevent non-agent jobs from re-emitting it.
// GH_AW_AIC may be propagated to downstream jobs via workflow outputs, so gate it
// behind jobEmitsOwnTokenUsage to prevent non-owning jobs from re-emitting it.
const aiCredits = jobEmitsOwnTokenUsage ? (normalizeNonNegativeNumber(process.env.GH_AW_AIC) ?? agentUsage.ai_credits) : undefined;
if (typeof aiCredits === "number" && aiCredits > 0) {
attributes.push(buildAttr("gh-aw.aic", aiCredits));
Expand Down Expand Up @@ -2302,7 +2302,7 @@ async function sendJobConclusionSpan(spanName, options = {}) {
}
}

// Only attach token-usage attributes to jobs that actually executed an agent.
// Only attach token-usage attributes to jobs that actually executed model usage.
// Most downstream jobs (conclusion, safe_outputs) may have agent_usage.json on
// disk via artifact download but must NOT emit token data — otherwise every
// sum(gen_ai.usage.*) query is inflated by the number of downstream jobs.
Expand Down
30 changes: 30 additions & 0 deletions actions/setup/js/send_otlp_span.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5351,6 +5351,36 @@ describe("sendJobConclusionSpan", () => {
expect(attrs["gh-aw.detection.conclusion"]).toBe("success");
});

it("includes engine-job token breakdown and cost attributes when the job name matches the engine id", async () => {
const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" });
vi.stubGlobal("fetch", mockFetch);

process.env.INPUT_JOB_NAME = "copilot";
process.env.GH_AW_INFO_ENGINE_ID = "copilot";
statSpy.mockImplementation(() => {
throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
});
process.env.GH_AW_OTLP_ENDPOINTS = JSON.stringify([{ url: "https://traces.example.com" }]);

const usage = { input_tokens: 5000, output_tokens: 200, cache_read_tokens: 100, ai_credits: 0.125 };
readFileSpy.mockImplementation(filePath => {
if (filePath === "/tmp/gh-aw/agent_usage.json") {
return JSON.stringify(usage);
}
throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
});

await sendJobConclusionSpan("gh-aw.copilot.conclusion");

const body = JSON.parse(mockFetch.mock.calls[0][1].body);
const attrs = Object.fromEntries(body.resourceSpans[0].scopeSpans[0].spans[0].attributes.map(a => [a.key, a.value.intValue ?? a.value.doubleValue ?? a.value.stringValue]));
expect(attrs["gen_ai.usage.input_tokens"]).toBe(5000);
expect(attrs["gen_ai.usage.output_tokens"]).toBe(200);
expect(attrs["gen_ai.usage.cache_read.input_tokens"]).toBe(100);
expect(attrs["gen_ai.usage.total_tokens"]).toBe(5200);
expect(attrs["gh-aw.aic"]).toBe(0.125);
});

it("includes detection-job warning result attribute when detection finds threats", async () => {
const mockFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: "OK" });
vi.stubGlobal("fetch", mockFetch);
Expand Down
Loading