Uh oh!
There was an error while loading. Please reload this page.
fix(providers): restore live provider pricing by bumping the Copilot SDK - #418
Conversation
get_model_pricing reads billing.token_prices, and github-copilot-sdk 1.0.1 -- the version uv.lock pinned -- parsed models.list with a hand-written client.ModelBilling that declared only `multiplier` and discarded the `tokenPrices` wire field. The field never left the API: generated/rpc.py still models it, and only the client dataclass drops it. So the hook returned None for every model, the chain #265 built (workflow override -> hook -> static table -> unpriced) ran permanently on its fallback, and any model missing from DEFAULT_PRICING reported no cost at all. 1.0.9 parses the field again. Verified against both wheels rather than assumed, and end to end: a model parsed by the real ModelInfo.from_dict now yields $15.00/$75.00 per million tokens for claude-opus-5 (1.5 credits per 1k tokens at 100 credits to the dollar). The floor moves to >=1.0.9 rather than relocking alone. The old >=1.0.0 is satisfied by 1.0.1, so an existing environment would keep dead pricing while reporting a healthy dependency. Diffed every SDK method conductor calls between the two versions by AST rather than by eye. create_session and resume_session gained 16 optional kwargs and lost nothing; list_models, start, stop, abort, destroy, disconnect, on, rpc, send and RuntimeConnection.for_uri are unchanged. The one narrowing is approve_all's `invocation`, now a PermissionInvocation TypedDict, which the default permission handler picks up. 1.0.9 also made approve_all raise when managed_settings_enabled is set; that is unreachable here, since the flag comes from the enable_managed_settings opt-in on create_session and conductor never passes it. Recorded in the docstring so the next reader does not have to trace it again. No per-token rates invented for claude-opus-5 or gpt-5.6-sol. With the hook alive they price from the SDK, which is the outcome #386 wanted without anyone guessing numbers. The existing hook tests build their models from SimpleNamespace, so they assert what conductor does with a billing object rather than whether the SDK still supplies one -- which is why 21 models went unpriced against a green suite. Two tests now build the model through the SDK's own ModelInfo.from_dict. Confirmed they fail on 1.0.1 and pass on 1.0.9, so the next release that drops the field breaks the build instead of quietly reverting every run to static pricing. 6091 passed, ruff and ty clean. Refs #386 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ache reads Review of the SDK bump turned up three things the bump itself introduced or exposed. approve_all is no longer unconditional in >=1.0.9. It abstains with PermissionNoResult when the runtime marks a request managed_approval_required, and raises when managed settings are enabled. The first is the dangerous one: managed_approval_required is a wire field the server sets, not an opt-in Conductor controls, so it can arrive on any run. The sentinel exists so a second connected client can answer instead -- and Conductor is the only client, so forwarding it leaves the request unanswered. The CLI blocks, idle recovery spends five inert prompts on a session that is waiting for a permission decision rather than for text, and ~9 minutes later the run dies with an error naming the network, the SDK and the agent, none of which are at fault. Both branches now decline explicitly and say why. Declining rather than approving is deliberate. Managed approval is a policy control; approving through it would convert a hang into a bypass. The RuntimeError is guarded for a different reason than it looks. It is unreachable today -- the flag comes from an enable_managed_settings opt-in on create_session that Conductor never passes -- but the SDK catches exceptions raised inside this callback, logs them to its own logger, and answers PermissionDecisionUserNotAvailable itself. Since Conductor installs no logging handlers, an unguarded raise would surface as every tool being silently denied. Documenting the unreachability was not enough. 1.0.9 also deprecates token_prices.cache_price in favour of separate cache_read_price and cache_write_price. The hook read only the deprecated field, so a model shipping just the replacements priced cache reads at $0.00 with every other figure correct -- #386 one field over. Read the new fields, keep the old one as the fallback, and populate cache writes, which were hardcoded to zero on the claim that Copilot has no write rate. The CI step that strips the bundled CLI binary had quietly become a no-op: the 1.0.9 wheel is pure-Python, 478KB against six ~90MB platform wheels, and fetches the CLI on first use. A test reaching the real CLI path would now download it inside a 10-minute job before hitting the auth hang the step existed to prevent. COPILOT_SKIP_CLI_DOWNLOAD blocks that; the step stays for venvs restored from an older lock, with a comment that matches reality. CHANGELOG corrected on two counts: token_prices was restored in 1.0.7, not 1.0.9, so the floor is the tested version rather than the forced one, and the user-visible effect now leads instead of trailing six lines of SDK internals. Tests: the permission handler had no behavioural coverage at all -- every reference asserted only that it was passed to create_session, which is exactly why the abstention went unnoticed. All three branches are pinned now, plus both cache-field shapes. Verified by mutation: reverting each of the three fixes fails a test, and each fails only its own. 6096 passed, ruff and ty clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…provider-pricing # Conflicts: # .github/workflows/ci.yml # CHANGELOG.md
Jason Robert (jrob5756)
commented
Aug 12, 2026
Merged #390 hardened the "Remove bundled Copilot CLI binary" step to fail when removed=$(find .venv -path '*/copilot/bin/copilot*' -print -delete)if [ -z"$removed" ];thenecho"::error::No bundled Copilot CLI matched ..."exit 1
fiThe reasoning is right, and it is the same defect I found from the other end: a silent no-op reinstates the auth hang the step exists to prevent. But the assertion stops being true with this PR. SDK 1.0.1 ships per-platform wheels carrying So taking that side unchanged would have turned Resolution keeps everything #390 added — Exercised the resolved script against all three layouts before pushing: old POSIX ( The env var sits on the test step, so it covers the Windows matrix entry #390 added as well as the Linux ones. CHANGELOG conflict was the ordinary both-sides-appended kind; kept both, no wording changed. 6129 passed locally after the merge, ruff and ty clean. |
Uh oh!
There was an error while loading. Please reload this page.
Refs #386. Fixes the mechanism; the issue also asks about static-table entries, which this deliberately does not touch (see below).
What was actually wrong
CopilotProvider.get_model_pricingreadsbilling.token_prices. The pinnedgithub-copilot-sdk1.0.1 ships twoModelBillingclasses:from_dictcopilot/generated/rpc.pymultiplier,token_pricestokenPricescopilot/client.pymultiplieronlymultiplier, dropstokenPricesclient.list_models()returns the second one, which is what the hook reads. So the field never left the API and was still modelled in the generated types; the client dataclass just threw it away.The effect is that the hook returned
Nonefor every model. The chain #265 built — workflow override, then hook, then static table, then unpriced — ran permanently on its fallback, and any model missing fromDEFAULT_PRICINGreported no cost at all. #388 made that visible; this makes it stop happening.1.0.9 parses the field again. Verified against both wheels rather than taken on trust, and end to end: a model parsed by the real
ModelInfo.from_dictyields$15.00/$75.00per million tokens forclaude-opus-5, which is 1.5 credits per 1k tokens at 100 credits to the dollar.Why the floor moves, not just the lock
>=1.0.0is satisfied by 1.0.1, so an existing environment would keep dead pricing while reporting a perfectly healthy dependency. The feature genuinely requires 1.0.9, so the floor should say so.Is the bump safe
I diffed every SDK method Conductor calls between the two versions by AST rather than by eye:
create_sessionandresume_sessiongained 16 optional kwargs and lost nothinglist_models,start,stop,abort,destroy,disconnect,on,rpc,send,RuntimeConnection.for_uriare unchangedapprove_all'sinvocationnarrowed fromdict[str, str]to aPermissionInvocationTypedDictThat last one is the
tyerror Frank Li (@franklixuefei) reported on #388.PermissionInvocationis a TypedDict, so it was always dict-compatible at runtime; the annotation now matches.One thing worth flagging that the type error hid: 1.0.9 also gave
approve_allbehaviour it did not have before.That flag is populated from
enable_managed_settings, an opt-in kwarg oncreate_sessionthat Conductor never passes, so the raise is unreachable. I recorded it in the handler docstring rather than leave the next person to trace it. If we ever do opt in, the blanket approve-all handler becomes a hard failure and will need to handle it.The static table
claude-opus-5andgpt-5.6-solare still absent, on purpose. With the hook alive they price from the SDK, so #386's practical complaint is answered without anyone inventing per-token rates — which is the trade Frank Li (@franklixuefei) declined to make on #388, correctly. Guessed rates are worse than no rates.Tests
The existing hook tests build their models from
SimpleNamespace, so they assert what Conductor does with a billing object, never whether the SDK still hands it one. That is why 21 models silently went unpriced against a fully green suite.Two new tests build the model through the SDK's own
ModelInfo.from_dict. I confirmed they fail on 1.0.1 and pass on 1.0.9, so the next SDK release that drops the field fails the build instead of quietly reverting every run to static pricing.Validation
6091 passed, 47 skipped.
ruff check,ruff format --checkandty check srcall clean.