Skip to content

feat(OPEN-11243): add GitHub Copilot SDK tracing - #223

Open
viniciusdsmello wants to merge 1 commit into
mainfrom
vini/open-11243-integration-github-copilot-sdk
Open

feat(OPEN-11243): add GitHub Copilot SDK tracing#223
viniciusdsmello wants to merge 1 commit into
mainfrom
vini/open-11243-integration-github-copilot-sdk

Conversation

@viniciusdsmello

@viniciusdsmello viniciusdsmello commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Traces agents built on the GitHub Copilot SDK (@github/copilot-sdk) to Openlayer. TypeScript counterpart to openlayer-ai/openlayer-python#669, structurally mirroring copilot_sdk.py so the two stay comparable.

import { traceCopilot } from "openlayer/lib/integrations/copilotSdk";
traceCopilot();

patches CopilotClient.prototype.createSession, so every session is traced with no change to the code that builds them. openlayerEventHandler() is the explicit alternative for callers who build sessions themselves. Both compose with a user-supplied onEvent and never replace it; an exception in either handler cannot break the other.

Trace shape

One trace per send():

AGENT  "GitHub Copilot"              user prompt in, final answer out
├─ CHAT_COMPLETION  "turn 0"         model, provider, tokens, latency
├─ TOOL  "bash"                      arguments in, result out
├─ AGENT "subagent: Explore Agent"   a `task` dispatch
│   ├─ CHAT_COMPLETION "turn 0"
│   └─ TOOL "view"
└─ CHAT_COMPLETION  "turn 1"

Every trace carries the Copilot session id, so multi-turn conversations group into one session.

Why buffer instead of building live

assistant.usage — which carries every token count — is ephemeral and absent from getEvents(), so we must subscribe live. But Copilot fires tool calls concurrently, with completions arriving out of order, so building steps from the live callbacks would nest siblings inside one another. We buffer live and build the whole trace in one deterministic, correctly-nested pass at session.idle.

Cost

Copilot's cost field is premium-request units, not dollars, so it is recorded as metadata. We emit provider + model and let Openlayer price it, mapping the model prefix to the real vendor — llm-costs has no github provider, so labelling it that way would silently yield $0.

GitHub meters each call in AIU and ships its own per-token rates, which turn out to be the vendor's list prices scaled by exactly 100 (1 AIU = $0.01), verified on both an Anthropic and an OpenAI model. Each chat step records copilot_metered_cost_usd so the priced cost is checkable per row; on live traces the two agree to twelve decimal places. An unmapped model falls back to that figure rather than landing at $0.

Packaging

New subpath export ./integrations/copilot-sdk, with @github/copilot-sdk as an optional peerDependency (^1.0.11) — mirroring how @anthropic-ai/claude-agent-sdk is handled.

yarn.lock grows because the SDK pulls platform-specific CLI binaries. The lockfile records all platforms (darwin / linux / linuxmusl / win32 × arm64/x64), so --frozen-lockfile resolves on a Linux CI runner.

Testing

26 unit tests over the same two captured sessions used on the Python side, converted to the camelCase shape this binding delivers. Coverage: concurrent tools, subagent nesting, the apiCallId usage join, the token partition, handler composition, and the patch itself.

Plus a live end-to-end test gated on OPENLAYER_COPILOT_LIVE_TEST=1 — deliberately not on GITHUB_TOKEN, which developers and unrelated CI steps commonly export and which, as an installation token, has no Copilot access.

Verified against real ingest: a published row comes back with provider: openai, model: gpt-5-mini, cost: 0.00139005, and copilot_metered_cost_usd matching to 1e-12.

Baseline preserved (the one pre-existing openai-tracer.test.ts failure is untouched); lint and tsc clean.

Example

examples/copilot-sdk-tracing.ts — basic session, client-side tool with a subagent dispatch, and multi-turn session grouping. Verified running end to end against the real SDK.

Related

Deliberately does not use closes, since OPEN-11243 spans three PRs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01S48tdMHz7rjd7aJeZCkVic

Trace agents built on the GitHub Copilot SDK (`@github/copilot-sdk`) to
Openlayer. TypeScript counterpart to the Python integration, structurally
mirroring it so the two stay comparable.

    import { traceCopilot } from "openlayer/lib/integrations/copilotSdk";
    traceCopilot();

patches `CopilotClient.prototype.createSession`, so every session is
traced with no change to the code that builds them.
`openlayerEventHandler()` is the explicit alternative for callers who
build sessions themselves. Both compose with a user-supplied `onEvent`
and never replace it; an exception in either handler cannot break the
other. Combining the two is safe: the patch defers to a caller-supplied
Openlayer handler rather than adding a second collector, so mixing the
quickstart with the per-session snippet cannot produce duplicate rows.

Trace shape — one trace per `send()`:

    AGENT  "GitHub Copilot"              user prompt in, final answer out
    ├─ CHAT_COMPLETION  "turn 0"         model, provider, tokens, latency
    ├─ TOOL  "bash"                      arguments in, result out
    ├─ AGENT "subagent: Explore Agent"   a `task` dispatch
    │   ├─ CHAT_COMPLETION "turn 0"
    │   └─ TOOL "view"
    └─ CHAT_COMPLETION  "turn 1"

Architecture: buffer live, build deferred
-----------------------------------------
`assistant.usage` — which carries every token count — is ephemeral and
absent from `getEvents()`, so we must subscribe live. But Copilot fires
tool calls *concurrently*, with completions arriving out of order, so
building steps from the live callbacks would nest siblings inside one
another. We buffer live and build the whole trace in one deterministic,
correctly-nested pass at `session.idle`.

Cost
----
Copilot's `cost` field is premium-request units, not dollars, so it is
recorded as metadata. We emit provider+model and let Openlayer price it,
mapping the model prefix to the real vendor — `llm-costs` has no `github`
provider, so labelling it that way would silently yield $0.

GitHub meters each call in AIU and ships its own per-token rates, which
are the vendor's list prices scaled by exactly 100 (1 AIU = $0.01),
verified on both an Anthropic and an OpenAI model. Each chat step records
`copilot_metered_cost_usd` so the priced cost is checkable per row; on
live traces the two agree to twelve decimal places. An unmapped model
falls back to that figure rather than landing at $0.

Packaging
---------
New subpath export `./integrations/copilot-sdk`, with
`@github/copilot-sdk` declared as an optional peerDependency (^1.0.11).
The lockfile carries all platform binaries, so a Linux CI runner resolves.

Tests
-----
28 unit tests over the same two captured sessions (converted to the
camelCase shape this binding delivers) covering concurrent tools,
subagent nesting, the apiCallId usage join, the token partition, handler
composition, and the patch itself. Plus a live end-to-end test gated on
OPENLAYER_COPILOT_LIVE_TEST=1.

Example: examples/copilot-sdk-tracing.ts, verified running end to end.

Related: openlayer-python (Python parity), openlayer-docs (docs page).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S48tdMHz7rjd7aJeZCkVic
@viniciusdsmello
viniciusdsmello force-pushed the vini/open-11243-integration-github-copilot-sdk branch from a77421d to 66f314e Compare August 29, 2026 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant