Skip to content

feat(pricing): port getPricing, setPricingTable and listKnownModels - #4

Merged
Goldokpa merged 2 commits into
identArk:mainfrom
Kem-Ada:feat/pricing-parity
Sep 23, 2026
Merged

Goldokpa merged 2 commits into
identArk:mainfrom
Kem-Ada:feat/pricing-parity

Conversation

@Kem-Ada

@Kem-Ada Kem-Ada commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Brings src/pricing.ts to parity with the Python SDK's identark/pricing.py. Before this change
the TS module was a static lookup table plus estimateCost; the Python module has an active
pricing table that can be overridden three ways. This PR ports that model.

  • Adds the ModelPricing interface, replacing the untyped Record<string, number> values.
  • Adds 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
    composes all four provider tables into BUNDLED_PRICING — 18 models, matching Python.
  • Adds getPricing, setPricingTable and listKnownModels over the active table.
  • Supports the same overrides at the same precedence as Python:
    IDENTARK_PRICING_URL > ~/.identark/pricing.json > bundled defaults.
  • Rewires estimateCost onto the active table, so an override actually reaches the cost_usd
    figures DirectGateway reports (it calls estimateCost at src/gateways/direct.ts:301,389,498,562).

Two pre-existing estimateCost bugs fall out of that rewire, both parity breaks against Python's
dict.get:

  1. Falsy-zero rates. The old guard if (!rates || !rates.input || !rates.output) treated a model
    priced at {input: 0, output: 0} as unpriced and billed it at the $10/1M unknown-model
    fallback. Python returns 0.0. Now covered by two tests.
  2. Inherited Object.prototype members. pricing["toString"] returned the inherited function,
    so estimateCost("toString", …) computed NaN and silently poisoned session cost totals.
    getPricing now gates on Object.hasOwn.

Neither was reachable before this PR without a hand-written model name, but setPricingTable makes
user-supplied keys a normal path, so both are fixed here rather than deferred.

The existing OPENAI_PRICING / ANTHROPIC_PRICING / MISTRAL_PRICING exports are kept as-is —
they are public API at v1.0.3, so consolidating them into BUNDLED_PRICING would 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, new tests/pricing.test.ts.

Python (identark/pricing.py) TypeScript (src/pricing.ts) Notes
class ModelPricing(TypedDict) export interface ModelPricing input / output, USD per 1M tokens
_BUNDLED_PRICING (18 models) BUNDLED_PRICING, composed from the four provider tables same 18 models, same rates
_pricing_table, _initialized module-scope let pricingTable, let initialized same caching semantics
_load_local_overrides() loadLocalOverrides() — node:os homedir() + node:fs readFileSync same path ~/.identark/pricing.json; same swallow-and-warn on parse/IO error
_fetch_remote_pricing(url) (sync urllib, 5 s) fetchRemotePricing(url) — fetch + AbortSignal.timeout(5000) same 5 s budget; async (see below)
_initialize() initializeSync() + export async function initPricing() see "The one divergence"
get_pricing(model) -> ModelPricing | None getPricing(model): ModelPricing | undefined None → undefined
set_pricing_table(table) setPricingTable(table) merges over bundled, sets initialized
list_known_models() -> list[str] listKnownModels(): string[] Object.keys of the active table
estimate_cost(model, input_tokens, output_tokens, provider="openai") estimateCost(model, inputTokens, outputTokens, provider = "openai") now reads the active table
logger.warning(...) console.warn("identark.pricing: …") the TS SDK has no logger; logger.info/logger.debug calls are dropped rather than made unconditional stdout noise

The one divergence. Python's _initialize() is synchronous because urllib.request.urlopen
is, which lets get_pricing transparently fetch the remote table on first use. Node has no
synchronous HTTP primitive, so exact parity is unreachable. The split taken here:

  • initializeSync() runs on every getPricing / listKnownModels call and handles the
    ~/.identark/pricing.json layer. getPricing and estimateCost stay synchronous, so nothing
    downstream — including DirectGateway's cost tracking — changes shape.
  • await initPricing() performs the full Python _initialize(), remote fetch included. Callers
    who set IDENTARK_PRICING_URL must call it once at startup.

Python's precedence is preserved exactly, including the elif: when IDENTARK_PRICING_URL is set,
the lower-priority local file is never read, and a failed fetch leaves bundled defaults rather
than falling through. initializeSync therefore returns early without marking initialization
done when a URL is configured, so a later initPricing() can still apply the remote table.

Test plan

Real gate output, run from inside identark-js on feat/pricing-parity:

$ npm ci
added 235 packages, and audited 236 packages in 6s

$ npm run lint
> identark@1.0.3 lint
> eslint src --max-warnings 0
(clean)

$ npm run typecheck
> identark@1.0.3 typecheck
> tsc --noEmit
(clean)

$ npx vitest run
 ✓ tests/types.test.ts              (5 tests)   5ms
 ✓ tests/mock-gateway.test.ts       (12 tests)  12ms
 ✓ tests/control-plane.test.ts      (17 tests)  18ms
 ✓ tests/vercel-integration.test.ts (5 tests)   15ms
 ✓ tests/managed-tools.test.ts      (2 tests)   13ms
 ✓ tests/direct-gateway.test.ts     (13 tests)  19ms
 ✓ tests/pricing.test.ts            (35 tests)  440ms

 Test Files  7 passed (7)
      Tests  89 passed (89)
   Duration  1.50s

$ ../scripts/check_scope.sh
check_scope: OK — no control-plane code touched.

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.ts mirrors all 8 Python tests in tests/unit/test_pricing.py and adds
negative coverage the Python suite lacks:

Positive (mirroring Python): known-model shape and rates for gpt-4o, claude-3-5-sonnet-20241022
and mistral-large-latest; estimateCost arithmetic; provider: "local" → 0; unknown-model
fallback arithmetic; listKnownModels membership; setPricingTable override with bundled models
still reachable.

Negative / TS-only: unknown model → undefined; empty-string model; Object.prototype member
names; malformed ~/.identark/pricing.json falls 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 once
across repeated initPricing() calls; setPricingTable suppresses both file and remote layers;
zero-priced and half-free models; zero tokens; BUNDLED_PRICING not mutated by setPricingTable.

Tests are hermetic: homedir() is mocked to a fresh mkdtempSync directory per test, so a real
~/.identark/pricing.json on the developer's machine cannot affect the suite. Module-level cache
state is handled with vi.resetModules() plus a dynamic re-import per test, so no test-only reset
hook is added to the production API.

Control-plane: untouched (check_scope OK)

🤖 Generated with Claude Code

Kem-Ada and others added 2 commits September 21, 2026 15:58
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
Goldokpa merged commit 892627b into identArk:main Sep 23, 2026
4 checks passed
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>
@Goldokpa Goldokpa mentioned this pull request Oct 27, 2026
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.

2 participants