Repository navigation
feat(pricing): port getPricing, setPricingTable and listKnownModels - #4
Merged
Merged
Conversation
Brings src/pricing.ts to parity with the Python SDK's identark/pricing.py.
- Add the `ModelPricing` interface, replacing `Record<string, number>` values.
- Add the five bundled models the TS table was missing (`o1`, `o1-mini`,
`claude-sonnet-4-20250514`, `claude-opus-4-20250514`) plus a new
`GEMINI_PRICING` table, and compose them into `BUNDLED_PRICING`.
- Add `getPricing`, `setPricingTable` and `listKnownModels` over an active,
override-aware pricing table.
- Support the same overrides as Python, at the same precedence:
`IDENTARK_PRICING_URL` > `~/.identark/pricing.json` > bundled defaults.
Node has no synchronous HTTP primitive, so the remote fetch is exposed as
`await initPricing()`; the local-file and `setPricingTable` paths stay sync.
- Rewire `estimateCost` onto the active table so overrides reach the cost
figures DirectGateway reports.
Two parity bugs in `estimateCost` fall out of the rewire. A model priced at
`{input: 0, output: 0}` was billed at the unknown-model fallback rate because
of a falsy check on the rates, and a model named after an `Object.prototype`
member resolved to the inherited value and produced a `NaN` cost. Python's
`dict.get` does neither.
The existing `OPENAI_PRICING` / `ANTHROPIC_PRICING` / `MISTRAL_PRICING`
exports are kept as-is for backwards compatibility.
Tests: 35 new cases in tests/pricing.test.ts covering the eight Python unit
tests in tests/unit/test_pricing.py plus negative cases for unknown models,
malformed and missing override files, failing and non-OK remote fetches,
override precedence, and zero-priced models.
Co-Authored-By: Claude <noreply@anthropic.com>
Goldokpa
added a commit
that referenced
this pull request
Oct 9, 2026
Brings src/pricing.ts to parity with the Python SDK's identark/pricing.py.
- Add the `ModelPricing` interface, replacing `Record<string, number>` values.
- Add the five bundled models the TS table was missing (`o1`, `o1-mini`,
`claude-sonnet-4-20250514`, `claude-opus-4-20250514`) plus a new
`GEMINI_PRICING` table, and compose them into `BUNDLED_PRICING`.
- Add `getPricing`, `setPricingTable` and `listKnownModels` over an active,
override-aware pricing table.
- Support the same overrides as Python, at the same precedence:
`IDENTARK_PRICING_URL` > `~/.identark/pricing.json` > bundled defaults.
Node has no synchronous HTTP primitive, so the remote fetch is exposed as
`await initPricing()`; the local-file and `setPricingTable` paths stay sync.
- Rewire `estimateCost` onto the active table so overrides reach the cost
figures DirectGateway reports.
Two parity bugs in `estimateCost` fall out of the rewire. A model priced at
`{input: 0, output: 0}` was billed at the unknown-model fallback rate because
of a falsy check on the rates, and a model named after an `Object.prototype`
member resolved to the inherited value and produced a `NaN` cost. Python's
`dict.get` does neither.
The existing `OPENAI_PRICING` / `ANTHROPIC_PRICING` / `MISTRAL_PRICING`
exports are kept as-is for backwards compatibility.
Tests: 35 new cases in tests/pricing.test.ts covering the eight Python unit
tests in tests/unit/test_pricing.py plus negative cases for unknown models,
malformed and missing override files, failing and non-OK remote fetches,
override precedence, and zero-priced models.
Co-authored-by: Kem-Ada <283358351+Kem-Ada@users.noreply.github.com>
Co-authored-by: Gold okpa <Okpagold@gmail.com>
Merged
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
Brings
src/pricing.tsto parity with the Python SDK'sidentark/pricing.py. Before this changethe TS module was a static lookup table plus
estimateCost; the Python module has an activepricing table that can be overridden three ways. This PR ports that model.
ModelPricinginterface, replacing the untypedRecord<string, number>values.o1,o1-mini,claude-sonnet-4-20250514,claude-opus-4-20250514) plus a newGEMINI_PRICINGtable, andcomposes all four provider tables into
BUNDLED_PRICING— 18 models, matching Python.getPricing,setPricingTableandlistKnownModelsover the active table.IDENTARK_PRICING_URL>~/.identark/pricing.json> bundled defaults.estimateCostonto the active table, so an override actually reaches thecost_usdfigures
DirectGatewayreports (it callsestimateCostatsrc/gateways/direct.ts:301,389,498,562).Two pre-existing
estimateCostbugs fall out of that rewire, both parity breaks against Python'sdict.get:if (!rates || !rates.input || !rates.output)treated a modelpriced at
{input: 0, output: 0}as unpriced and billed it at the$10/1Munknown-modelfallback. Python returns
0.0. Now covered by two tests.Object.prototypemembers.pricing["toString"]returned the inherited function,so
estimateCost("toString", …)computedNaNand silently poisoned session cost totals.getPricingnow gates onObject.hasOwn.Neither was reachable before this PR without a hand-written model name, but
setPricingTablemakesuser-supplied keys a normal path, so both are fixed here rather than deferred.
The existing
OPENAI_PRICING/ANTHROPIC_PRICING/MISTRAL_PRICINGexports are kept as-is —they are public API at v1.0.3, so consolidating them into
BUNDLED_PRICINGwould be breaking.Python→TS parity mapping
Reference:
identark/pricing.py(147 lines),tests/unit/test_pricing.py(8 tests).Target:
src/pricing.ts,src/index.ts, newtests/pricing.test.ts.identark/pricing.py)src/pricing.ts)class ModelPricing(TypedDict)export interface ModelPricinginput/output, USD per 1M tokens_BUNDLED_PRICING(18 models)BUNDLED_PRICING, composed from the four provider tables_pricing_table,_initializedlet pricingTable,let initialized_load_local_overrides()loadLocalOverrides()—node:oshomedir()+node:fsreadFileSync~/.identark/pricing.json; same swallow-and-warn on parse/IO error_fetch_remote_pricing(url)(syncurllib, 5 s)fetchRemotePricing(url)—fetch+AbortSignal.timeout(5000)_initialize()initializeSync()+export async function initPricing()get_pricing(model) -> ModelPricing | NonegetPricing(model): ModelPricing | undefinedNone→undefinedset_pricing_table(table)setPricingTable(table)initializedlist_known_models() -> list[str]listKnownModels(): string[]Object.keysof the active tableestimate_cost(model, input_tokens, output_tokens, provider="openai")estimateCost(model, inputTokens, outputTokens, provider = "openai")logger.warning(...)console.warn("identark.pricing: …")logger.info/logger.debugcalls are dropped rather than made unconditional stdout noiseThe one divergence. Python's
_initialize()is synchronous becauseurllib.request.urlopenis, which lets
get_pricingtransparently fetch the remote table on first use. Node has nosynchronous HTTP primitive, so exact parity is unreachable. The split taken here:
initializeSync()runs on everygetPricing/listKnownModelscall and handles the~/.identark/pricing.jsonlayer.getPricingandestimateCoststay synchronous, so nothingdownstream — including
DirectGateway's cost tracking — changes shape.await initPricing()performs the full Python_initialize(), remote fetch included. Callerswho set
IDENTARK_PRICING_URLmust call it once at startup.Python's precedence is preserved exactly, including the
elif: whenIDENTARK_PRICING_URLis set,the lower-priority local file is never read, and a failed fetch leaves bundled defaults rather
than falling through.
initializeSynctherefore returns early without marking initializationdone when a URL is configured, so a later
initPricing()can still apply the remote table.Test plan
Real gate output, run from inside
identark-jsonfeat/pricing-parity:54 tests / 6 files → 89 tests / 7 files (+35). The 54/6 baseline in Section 2 of the brief is
reproduced exactly by the six pre-existing files above, so nothing regressed.
tests/pricing.test.tsmirrors all 8 Python tests intests/unit/test_pricing.pyand addsnegative coverage the Python suite lacks:
Positive (mirroring Python): known-model shape and rates for
gpt-4o,claude-3-5-sonnet-20241022and
mistral-large-latest;estimateCostarithmetic;provider: "local"→0; unknown-modelfallback arithmetic;
listKnownModelsmembership;setPricingTableoverride with bundled modelsstill reachable.
Negative / TS-only: unknown model →
undefined; empty-string model;Object.prototypemembernames; malformed
~/.identark/pricing.jsonfalls back to bundled and warns once; missing file;remote fetch that rejects; remote fetch returning HTTP 500; local file ignored when a pricing URL is
set; local file not applied when a URL is set but
initPricing()never called; fetch happens onceacross repeated
initPricing()calls;setPricingTablesuppresses both file and remote layers;zero-priced and half-free models; zero tokens;
BUNDLED_PRICINGnot mutated bysetPricingTable.Tests are hermetic:
homedir()is mocked to a freshmkdtempSyncdirectory per test, so a real~/.identark/pricing.jsonon the developer's machine cannot affect the suite. Module-level cachestate is handled with
vi.resetModules()plus a dynamic re-import per test, so no test-only resethook is added to the production API.
Control-plane: untouched (check_scope OK)
🤖 Generated with Claude Code