feat: multi-provider foundation (SessionRef extension + provider pricers)#20
Merged
0bserver07 merged 2 commits intomainfrom Apr 30, 2026
Merged
feat: multi-provider foundation (SessionRef extension + provider pricers)#200bserver07 merged 2 commits intomainfrom
0bserver07 merged 2 commits intomainfrom
Conversation
…ers)
Implements steps 1+2 of docs/specs/multi-provider/spec.md — the prerequisite
work for Wave 2 (Cursor + Cline adapters). No new providers in this PR.
§1 adapter contract:
- SessionRef gains optional source_kind ("file" | "database") and
source_hint dict; defaults keep existing JSONL adapters working unchanged.
- ingest_log migrated from `file_path PRIMARY KEY` to a new shape with
session_id, storage_kind, last_rowid columns plus two partial unique
indexes (one for file-mode session_id IS NULL rows, one for db-mode
session_id IS NOT NULL rows) so SQLite's NULL-distinct-in-UNIQUE
behaviour doesn't let duplicate file rows in. Existing rows survive
with session_id=NULL, storage_kind='file', last_rowid=NULL.
- run_ingest branches on ref.source_kind to compute resume offset; writer
stores max(seq) for both kinds (rowid for db, byte offset of last line
for file). Adapters now skip records with seq <= since_offset so the
storage-aware contract test from spec §1.4 passes for both Claude and
Codex fixtures.
§2 provider pricers:
- New infra/providers/ package: ProviderPricer ABC + AnthropicPricer +
OpenAIPricer, registered by name with singleton instances and an
Anthropic fallback for unknown providers.
- infra/costs.compute_cost is now a thin shim:
pricer.normalize_tokens(...) → pricer.compute(...). Default
provider="anthropic" keeps every existing call site (~10 in
stats/aggregator.py, plus routes/cost, store/queries) working.
- Codex's cached-input subtraction moved out of adapters/codex.py and
into OpenAIPricer.normalize_tokens. Adapter now delegates to the
pricer instead of inlining the subtract+fold-reasoning logic. A
parametrised regression test (test_codex_cost_equivalence.py) proves
the move is cost-neutral against the legacy normalisation.
- Aggregator collectors take provider= per project; the value is
resolved once from ds.records[0].provider (records carry it through
from RawEntry/TaggedEntry, populated from projects.provider in
build_enriched_dataset).
Tests: 484 passing, 2 skipped (was 444 + 2 baseline; +40 covering the
new pricers, the contract test, the migration, and the regression
fixture). Frontend typecheck + build clean.
Conservative-equivalence note for the PR body: the Codex adapter still
emits 4-slot canonical token shape into Record (input_tokens excludes
cached, output_tokens includes reasoning) so the SQLite messages table
and per-message cache stats see the same values as before. The
flattening logic moved to OpenAIPricer.normalize_tokens — the adapter
calls into the pricer rather than inlining the math, satisfying
"normalization lives in the pricer." A future change can switch the
adapter to emit raw shape and have the pricer flatten on read.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0bserver07
added a commit
that referenced
this pull request
Apr 30, 2026
…UI polish Wave 2 of the multi-provider initiative. Extends the adapter contract to support SQLite-backed and vscdb sources alongside the existing JSONL flow, ships beta adapters for Cursor and Cline (both opt-in via env var), and adds provider chips + estimated-cost markers to the dashboard. Bundles PRs #20, #21, #22, #23, #24. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
0bserver07
added a commit
that referenced
this pull request
Apr 30, 2026
…UI polish (#25) Wave 2 of the multi-provider initiative. Extends the adapter contract to support SQLite-backed and vscdb sources alongside the existing JSONL flow, ships beta adapters for Cursor and Cline (both opt-in via env var), and adds provider chips + estimated-cost markers to the dashboard. Bundles PRs #20, #21, #22, #23, #24. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
Implements Steps 1 + 2 of
docs/specs/multi-provider/spec.md— the prerequisite work so Wave 2 (Cursor + Cline adapters) can land without further refactoring of the adapter contract or cost layer. No new providers in this PR.§1 — Adapter contract extension
SessionRefgains optionalsource_kind: Literal["file", "database"] = "file"andsource_hint: dict[str, Any] | None = None. JSONL adapters need zero changes.ingest_logmigrated fromfile_path PRIMARY KEYto(id INTEGER PRIMARY KEY, file_path, session_id TEXT, storage_kind TEXT CHECK(...), mtime, size, processed_offset NULLABLE, last_rowid NULLABLE, last_ingest_ts)plus two partial unique indexes — one for file-mode rows (session_id IS NULL), one for database-mode rows. Existing rows survive withsession_id=NULL,storage_kind='file',last_rowid=NULL. Migration is viaCREATE … INSERT SELECT … DROP … RENAMEinside one transaction; idempotent becauseapply()readsPRAGMA user_versionand skips already-applied migrations.run_ingest()branches onref.source_kind: file-mode resumes viaprocessed_offsetkeyed onWHERE session_id IS NULL; database-mode resumes vialast_rowidkeyed on(file_path, session_id).max(record.seq)for both kinds — for database mode that's a rowid, for file mode it's the byte offset of the last consumed line. Adapters skip records withseq <= since_offsetso the storage-aware contract test (test_read_since_offset_is_storage_aware, spec §1.4) passes for both providers.§2 — Provider pricer scaffold
stackunderflow/infra/providers/package:ProviderPricerABC plusAnthropicPricerandOpenAIPricerextracted from the oldinfra/costs.pyheuristic.get_pricer(provider)returns a singleton, falls back to Anthropic for unknown providers.infra/costs.compute_cost(tokens, model, provider="anthropic")is now a thin shim — the default keeps every existing call site working unchanged. Aggregator collectors take aprovider=constructor arg resolved once fromds.records[0].provider; records carry it through fromRawEntry/TaggedEntrypopulated fromprojects.providerinbuild_enriched_dataset.adapters/codex.pyintoOpenAIPricer.normalize_tokens. The adapter now delegates to the pricer instead of inlining the cached-input subtraction + reasoning fold. A parametrised regression test (tests/stackunderflow/infra/providers/test_codex_cost_equivalence.py) proves the move is cost-neutral by computing the same (model, raw-token) bundle with both the legacy convention (caller pre-normalises) and the new one (pricer normalises) — they match for 5 fixtures including a 100%-cached-input edge case.Conservative choice (not in spec)
The spec wanted
adapters/codex.pyto emit raw OpenAI shape intoRecord. The 4-slotRecorddataclass (input_tokens,output_tokens,cache_create_tokens,cache_read_tokens) doesn't have a clean place forreasoning_output_tokensto live without re-purposingcache_create_tokens(which would corrupt downstream cache stats). I chose the smallest-diff path: the adapter still emits 4-slot canonical shape into the DB, but the flattening logic now physically lives inOpenAIPricer.normalize_tokens— the adapter calls into the pricer instead of inlining the math. The cost-equivalence regression test guarantees the dollar number doesn't change. A future PR can switch the adapter to raw shape onceRecordgrows a fifth slot or the encoding question gets resolved.Test plan
pytest tests/ -q— 484 passed, 2 skipped (baseline was 444 + 2; +40 covering the new pricers, the contract test, the v002 migration, and the cost-equivalence regression).pytest tests/stackunderflow/adapters/contract.py tests/stackunderflow/adapters/test_claude.py tests/stackunderflow/adapters/test_codex.py -q— 29 passed.cd stackunderflow-ui && npm run typecheck— clean.cd stackunderflow-ui && npm run build— clean.~/.stackunderflow/store.db.🤖 Generated with Claude Code