feat(OPEN-11243): add GitHub Copilot SDK tracing - #669
Open
viniciusdsmello wants to merge 1 commit into
Open
Conversation
Trace agents built on the GitHub Copilot SDK (`github-copilot-sdk` on
PyPI, imported as `copilot`) to Openlayer.
from openlayer.lib import init
init()
is all it takes. The Copilot SDK is registered in the auto-instrument
registry alongside every other supported SDK, so `init()` detects it when
installed and patches `CopilotClient.create_session` — no Copilot-specific
function name to know. `init(auto_instrument=["copilot"])` narrows it,
`trace_copilot()` does the same directly, and `unpatch_all()` reverses it.
`openlayer_event_handler()` is the explicit alternative for callers who
want to choose which sessions are traced. 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"
Every trace carries the Copilot session id, so multi-turn conversations
group into one session rather than unrelated rows.
Architecture: buffer live, build deferred
-----------------------------------------
Two wire facts pull in opposite directions. `assistant.usage` — which
carries every token count — is ephemeral and absent from `get_events()`
(a 3-turn session emits 153 live events but replays only 19), so we must
subscribe live. But Copilot fires tool calls *concurrently*: three
`tool.execution_start` arrive before any completion, and completions come
back out of order, so building steps from the live callbacks would nest
siblings inside one another. We therefore buffer live and build the whole
trace in one deterministic, correctly-nested pass at `session.idle`.
This is the one place the Claude Agent SDK integration's pattern is
deliberately not copied — its `_ToolStepHandle` docstring notes it
assumes serial pre/post tool pairs, which does not hold here.
Cost
----
Copilot's `cost` field is premium-request units, not dollars — a flat
per-model multiplier, identical on every call regardless of size — so it
is recorded as metadata rather than published as cost. We emit
provider+model and let Openlayer price it, mapping the model prefix to
the real underlying vendor; `llm-costs` has no `github` provider at all,
so labelling it that way would silently yield $0.
That price is not an approximation. GitHub meters each call in AIU and
ships its own per-token rates on the wire, and those rates are the
vendor's list prices scaled by exactly 100 (1 AIU = $0.01) — verified on
both `claude-haiku-4.5` (Anthropic) and `gpt-5-mini` (OpenAI). Each chat
step records GitHub's figure as `copilot_metered_cost_usd`, so the priced
cost is independently checkable on every row; on live traces the two
agree to twelve decimal places. A model whose prefix we cannot map falls
back to GitHub's metered figure rather than landing at $0.
Tokens: `input_tokens` is a superset already containing cache reads and
writes, so `usage_details` is emitted as a non-overlapping partition —
which reproduces Copilot's own `_token_details` breakdown exactly.
Tests
-----
37 unit tests driven by two real captured sessions (55 and 93 events)
covering concurrent tools, subagent nesting, the apiCallId usage join,
the token partition, handler composition, the auto-instrument registry
entry, and the duplicate-trace guard. Plus a live end-to-end test, gated
on OPENLAYER_COPILOT_LIVE_TEST=1 and Python 3.11+ (the Copilot SDK's own
floor, while this SDK still supports 3.9 — which is why the unit tests
run off fixtures).
Two live-only shape bugs the fixtures structurally could not catch are
covered by explicit regression tests: the binding hands back
`SessionEventType.SESSION_START` (an Enum) rather than a string, and
nests `copilot_usage` as a dataclass rather than a dict.
Example: examples/tracing/copilot_sdk/copilot_sdk_tracing.ipynb
Related: openlayer-ts (TypeScript 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
force-pushed
the
vini/open-11243-integration-github-copilot-sdk
branch
from
August 29, 2026 18:37
0101cba to
7759c6e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Traces agents built on the GitHub Copilot SDK (
github-copilot-sdkon PyPI, imported ascopilot) to Openlayer.patches
CopilotClient.create_session, so every session is traced with no change to the code that builds them.openlayer_event_handler()is the explicit alternative for callers who build sessions themselves. Both compose with a user-suppliedon_eventand never replace it; an exception in either handler cannot break the other.Trace shape
One trace per
send():Every trace carries the Copilot session id, so multi-turn conversations group into one session rather than unrelated rows.
Why buffer instead of building live
Two wire facts pull in opposite directions:
assistant.usage— which carries every token count — isephemeraland absent fromget_events(). A 3-turn session emits 153 live events but replays only 19, so we have to subscribe live.tool.execution_startarrive before any completion, and completions come back out of order. Building steps from the live callbacks would nest siblings inside one another.So we buffer live and build the whole trace in one deterministic, correctly-nested pass at
session.idle. This is the one place the Claude Agent SDK integration's pattern is deliberately not copied — its_ToolStepHandledocstring notes it assumes serial pre/post tool pairs, which does not hold here.Cost
Copilot's
costfield is premium-request units, not dollars — a flat per-model multiplier, identical on every call regardless of size — so it is recorded as metadata rather than published as cost. We emit provider + model and let Openlayer price it, mapping the model prefix to the real underlying vendor.llm-costshas nogithubprovider at all, so labelling it that way would silently yield $0.That price is not an approximation. GitHub meters each call in AIU and ships its own per-token rates on the wire, and those rates are the vendor's list prices scaled by exactly 100 (1 AIU = $0.01):
claude-haiku-4.5gpt-5-miniEach chat step records GitHub's own figure as
copilot_metered_cost_usd, so the priced cost is independently checkable on every row — on live traces the two agree to twelve decimal places. A model whose prefix we cannot map falls back to that figure rather than landing at $0.Tokens:
input_tokensis a superset already containing cache reads and writes, sousage_detailsis emitted as a non-overlapping partition — which reproduces Copilot's own_token_detailsbreakdown exactly.Testing
33 unit tests driven by two real captured sessions (55 and 93 events), covering concurrent tools, subagent nesting, the
apiCallIdusage join, the token partition, handler composition, and the monkey-patch itself.Plus a live end-to-end test, gated on
OPENLAYER_COPILOT_LIVE_TEST=1and Python 3.11+ — the Copilot SDK's own floor, while this SDK still supports 3.9, which is why the unit tests run off fixtures rather than the real package.Two live-only shape bugs that fixtures structurally cannot catch have explicit regression tests: the binding hands back
SessionEventType.SESSION_START(an Enum) rather than a string, and nestscopilot_usageas a dataclass rather than a dict. Both silently produced empty output before being found by the live gate.Verified against real ingest — a published row comes back with
provider: anthropic, a populated per-categorycostDetails, the user's actual prompt, and the final answer as output.No new failures against baseline; lint clean.
Example
examples/tracing/copilot_sdk/copilot_sdk_tracing.ipynb— basic session, client-side tool with a subagent dispatch, and multi-turn session grouping.Related
Deliberately does not use
closes, since OPEN-11243 spans three PRs.🤖 Generated with Claude Code
https://claude.ai/code/session_01S48tdMHz7rjd7aJeZCkVic