Discovery — swarph_mesh.discovery

Cost discovery substrate: AIMLAPI catalog, Gemini Cloud Billing, Anthropic static pricing, OpenAI / xAI / DeepSeek cost reconciliation, model normalization, retirement registry.

Tier 1 stability. list_models, is_model_supported, get_model_info, fetch_*_pricing, fetch_*_cost_buckets, reconcile_*_cost signatures are part of the contract.

Tier 2 stability. normalize_*_id helpers, _RETIREMENT_REGISTRY keys (consumers may iterate).

Tier 3 stability. _ANTHROPIC_PRICING static table contents.

Model discovery — swarph_mesh.discovery (v0.6.0 architectural promotion).

Per drop’s DM #720 direction: replaces the static PRICING dict’s implicit “is this model_id real” check with a runtime catalog query. Closes the silent-mis-attribute trap from drop’s #728 obs (a) at the substrate layer.

Architecture (commander direction 2026-05-09): AIMLAPI as primary discovery, per-provider as fallback.

Primary: GET https://api.aimlapi.com/models — public, no auth, ~608 entries with structured info (id, developer, contextLength, maxTokens, type, aliases, tags). 24h TTL cache at module scope.

Fallback: per-provider /v1/models endpoints when AIMLAPI is unreachable. Each provider requires its own API key:

  • OpenAI: client.models.list() (requires OPENAI_API_KEY)

  • DeepSeek: same shape via base_url (requires DEEPSEEK_API_KEY)

  • xAI: same shape via base_url (requires XAI_API_KEY)

  • Anthropic: GET /v1/models (requires ANTHROPIC_API_KEY — note

    this is METERED auth, not the subscription path)

  • Google: models.list() via google-generativeai SDK

    (requires GEMINI_API_KEY)

Fallback fires per-provider; if one provider’s key is missing, that provider’s models simply don’t appear in the fallback list.

PRICING stays in adapter-local tables (the AIMLAPI /models response does NOT include pricing — only the HTML pricing page does, and we won’t HTML-scrape). The discovery module exposes ModelInfo records WITHOUT pricing; callers join against the local PRICING dict for cost information. Drift detection is a future v0.6.x stretch.

class swarph_mesh.discovery.ModelInfo(id, developer, context_length=None, max_tokens=None, name=None, description=None, type=None, aliases=<factory>, tags=<factory>, source='aimlapi')[source]

Bases: object

One row from the discovery catalog. Provider-agnostic shape; pricing intentionally absent (kept in adapter-local PRICING dicts since AIMLAPI doesn’t expose pricing via API).

Parameters:
id: str
developer: str
context_length: int | None = None
max_tokens: int | None = None
name: str | None = None
description: str | None = None
type: str | None = None
aliases: list[str]
tags: list[str]
source: str = 'aimlapi'
matches_developer(dev)[source]

Loose developer-name match (handles ‘Open AI’ vs ‘OpenAI’ vs ‘openai’ drift across catalogs).

Parameters:

dev (str)

Return type:

bool

swarph_mesh.discovery.invalidate_catalog()[source]

Force the next list_models call to re-fetch from AIMLAPI.

Return type:

None

swarph_mesh.discovery.list_models(*, provider=None, ttl_seconds=86400)[source]

Return models known to the catalog.

provider is the swarph-mesh adapter name (“openai”, “deepseek”, “claude”, “gemini”, “grok”) which maps to AIMLAPI’s developer field via _PROVIDER_TO_DEVELOPER. Pass None for the full catalog.

Cache TTL: 24h by default. Pass ttl_seconds=0 to force a fresh fetch (e.g., after publishing a new model and wanting to verify it appears).

Parameters:
  • provider (str | None)

  • ttl_seconds (int)

Return type:

list[ModelInfo]

swarph_mesh.discovery.is_model_supported(model_id, *, ttl_seconds=86400)[source]

Fast existence check. Used by adapters to validate model_id before dispatching to the underlying SDK — closes the silent-mis- attribute trap (drop’s #728 obs (a)) at the substrate layer.

Matches against id AND aliases (AIMLAPI catalogs both canonical IDs like claude-opus-4-7 and aliased forms like openai/gpt-3.5-turbo).

Parameters:
  • model_id (str)

  • ttl_seconds (int)

Return type:

bool

swarph_mesh.discovery.get_model_info(model_id, *, ttl_seconds=86400)[source]

Single-model lookup. Returns None when not in catalog.

Parameters:
  • model_id (str)

  • ttl_seconds (int)

Return type:

ModelInfo | None

swarph_mesh.discovery.normalize_xai_id(model_id)[source]

Strip xAI prefix (x-ai/) + -beta + dated build suffixes (-DD-DD). See swarph_mesh.adapters.grok._normalize_xai_id for adapter-local copy with the same semantics.

Parameters:

model_id (str)

Return type:

str

swarph_mesh.discovery.normalize_deepseek_id(model_id)[source]

Strip DeepSeek prefix (deepseek/) + version suffixes (-v3.1, -v3.2-terminus). See swarph_mesh.adapters.deepseek._normalize_deepseek_id for adapter-local copy.

Parameters:

model_id (str)

Return type:

str

swarph_mesh.discovery.normalize_model_id(provider, model_id)[source]

Provider-aware normalizer. Dispatches to the right per-provider helper. Provider names match the adapter name field ("openai", "grok", etc.).

Parameters:
  • provider (str)

  • model_id (str)

Return type:

str

swarph_mesh.discovery.is_retired(provider, model_id, *, today=None)[source]

Return True if the model is past its retirement date as of today (default: actual today UTC). Returns False for unregistered models (not retired) and for deprecated sentinel entries (still routable, just deprecated).

Parameters:
  • provider (str)

  • model_id (str)

  • today (date | None)

Return type:

bool

swarph_mesh.discovery.retirement_date(provider, model_id)[source]

Return the retirement-date string for a provider/model_id, OR None if not in the registry.

Parameters:
  • provider (str)

  • model_id (str)

Return type:

str | None

class swarph_mesh.discovery.ProviderPricing(provider, model_hint, sku_id, sku_description, input_per_mtok=None, output_per_mtok=None, tier_threshold_tokens=0, usage_unit=None, source='google-cloud-billing', verified_at=None)[source]

Bases: object

One row of pricing data for a provider+SKU combo.

Tiered models (e.g., Gemini Pro at >128K context) surface as multiple ProviderPricing records — one per tier band — with tier_threshold_tokens distinguishing them. Callers wanting “the price at N tokens” pick the highest-threshold record where tier_threshold_tokens <= N.

Parameters:
  • provider (str)

  • model_hint (str)

  • sku_id (str)

  • sku_description (str)

  • input_per_mtok (float | None)

  • output_per_mtok (float | None)

  • tier_threshold_tokens (int)

  • usage_unit (str | None)

  • source (str)

  • verified_at (str | None)

provider: str
model_hint: str
sku_id: str
sku_description: str
input_per_mtok: float | None = None
output_per_mtok: float | None = None
tier_threshold_tokens: int = 0
usage_unit: str | None = None
source: str = 'google-cloud-billing'
verified_at: str | None = None
swarph_mesh.discovery.invalidate_pricing(provider=None)[source]

Clear pricing cache. provider=None clears all.

Parameters:

provider (str | None)

Return type:

None

swarph_mesh.discovery.fetch_gemini_pricing(*, api_key=None, ttl_seconds=86400)[source]

Hit Cloud Billing Catalog API, return parsed ProviderPricing records for all Gemini SKUs.

Auth: api_key parameter, OR $GOOGLE_CLOUD_BILLING_API_KEY env, OR $GOOGLE_CLOUD_API_KEY env. Note: this is a separate API key from the Generative AI GEMINI_API_KEY used by the chat adapter — Cloud Billing requires Cloud Console project keys with billing-API scope enabled. Operators provisioning need:

gcloud services enable cloudbilling.googleapis.com # then create an API key in Cloud Console with Cloud Billing # API restriction; export as GOOGLE_CLOUD_BILLING_API_KEY

Returns empty list on auth failure (treat as “billing-API key not configured” — adapter PRICING tables stay authoritative).

Pagination: Google returns up to 5000 SKUs per page; service 241C-273D-49C8 typically has < 500 entries so a single page suffices. We still page defensively for forward-compat.

Parameters:
  • api_key (str | None)

  • ttl_seconds (int)

Return type:

list[ProviderPricing]

swarph_mesh.discovery.pricing_for_anthropic_model(model_id)[source]

Return a ProviderPricing record for an Anthropic model.

Source: _ANTHROPIC_PRICING static table (manually verified against claude.com/pricing on _ANTHROPIC_PRICING_VERIFIED_AT).

Returns None for unknown model_ids — callers should fall back to the adapter-local PRICING table (which mirrors a subset of this table for the modern model lineup).

The ProviderPricing shape carries all five Anthropic price dimensions in custom attributes; input_per_mtok is the base input rate, output_per_mtok is the output rate. Cache writes + hits are exposed via raw_pricing for callers that care.

Parameters:

model_id (str)

Return type:

ProviderPricing | None

swarph_mesh.discovery.list_anthropic_pricing()[source]

Return ProviderPricing records for every model in the static Anthropic table. Used by drift-detection cron + audit surfaces.

Return type:

list[ProviderPricing]

class swarph_mesh.discovery.DeepSeekBalance(total_balance, granted_balance, topped_up_balance, currency='USD', is_available=True, fetched_at=None, raw=None)[source]

Bases: object

Snapshot of a DeepSeek account’s credit balance.

Unlike OpenAI’s CostBucket and xAI’s XAICostBucket, DeepSeek’s API exposes only current balance — not historical per-day usage. Use fetched_at to track snapshots over time; diff successive balances to approximate windowed spend.

Parameters:
  • total_balance (float)

  • granted_balance (float)

  • topped_up_balance (float)

  • currency (str)

  • is_available (bool)

  • fetched_at (str | None)

  • raw (dict | None)

total_balance: float
granted_balance: float
topped_up_balance: float
currency: str = 'USD'
is_available: bool = True
fetched_at: str | None = None
raw: dict | None = None
swarph_mesh.discovery.fetch_deepseek_balance(*, api_key=None, timeout=10.0)[source]

Hit GET /user/balance on DeepSeek for current credit state.

api_key resolves from arg → $DEEPSEEK_API_KEY env → /home/ubuntu/deepseek/.env legacy fallback (matches DeepSeekAdapter._resolve_api_key shape). The Anthropic-protocol endpoint at /anthropic/v1/messages is documented in the adapter module docstring but not used by this balance primitive.

Returns None if no key is configured OR the call fails.

Note: DeepSeek can have multiple balance_infos entries (different currencies). We return the USD entry; callers needing other currencies can inspect raw.

Parameters:
Return type:

DeepSeekBalance | None

class swarph_mesh.discovery.XAICostBucket(start_time, end_time, total_usd=0.0, line_items=<factory>, raw_bucket=None)[source]

Bases: object

One time-bucket of xAI usage data per /v1/billing/teams/.../usage.

xAI’s response shape exposes line_items per bucket, each with a model description (e.g., “Chat grok-4-0709”) and aggregated usage. We extract sum-style cost data into total_usd when available.

Parameters:
start_time: str
end_time: str
total_usd: float = 0.0
line_items: list
raw_bucket: dict | None = None
swarph_mesh.discovery.fetch_xai_cost_buckets(*, start_time, end_time, management_key=None, team_id=None, time_unit='TIME_UNIT_DAY', timezone='Etc/GMT', timeout=15.0)[source]

Hit xAI’s POST /v1/billing/teams/{team_id}/usage for a date range.

start_time / end_time accept either ISO 8601 (e.g., "2026-05-01T00:00:00Z") or xAI’s native format ("2026-05-01 00:00:00"). ISO inputs are converted internally; timezone is supplied via the separate timezone parameter (default "Etc/GMT").

time_unit is one of xAI’s enum strings: TIME_UNIT_DAY, TIME_UNIT_HOUR, TIME_UNIT_MONTH, TIME_UNIT_CALENDAR_WEEK, TIME_UNIT_QUARTER_HOUR, TIME_UNIT_MINUTE, TIME_UNIT_SECOND, TIME_UNIT_NONE.

management_key resolves from arg → $XAI_MANAGEMENT_KEY env. team_id resolves from arg → $XAI_TEAM_ID env (obtained from xAI console — not discoverable via API).

PRIVILEGE BOUNDARY: management keys can manage API keys, view billing, configure spending limits. swarph-mesh does NOT persist the key to disk — env-only.

Returns [] if either credential is missing OR the call fails (4xx/5xx/network).

Parameters:
  • start_time (str)

  • end_time (str)

  • management_key (str | None)

  • team_id (str | None)

  • time_unit (str)

  • timezone (str)

  • timeout (float)

Return type:

list[XAICostBucket]

swarph_mesh.discovery.reconcile_xai_cost(*, start_time, end_time, swarph_attributed_usd=None, management_key=None, team_id=None)[source]

xAI parallel of reconcile_openai_cost(). Compares xAI’s actual billed costs against swarph-mesh’s attribution.jsonl total and returns drift report.

Returns same shape as reconcile_openai_cost but with xai_actual_usd instead of openai_actual_usd.

Parameters:
  • start_time (str)

  • end_time (str)

  • swarph_attributed_usd (float | None)

  • management_key (str | None)

  • team_id (str | None)

Return type:

dict

class swarph_mesh.discovery.CostBucket(start_time, end_time, total_usd, currency='usd', line_item_breakdown=<factory>, project_breakdown=<factory>, api_key_breakdown=<factory>, raw_results=<factory>)[source]

Bases: object

One day’s worth of OpenAI cost data per /v1/organization/costs.

The endpoint returns up to 7 buckets by default (configurable via limit). Each bucket aggregates spend across the bucket window. Optional groupings (line_item / project_id / api_key_id) populate the breakdown dicts.

Parameters:
start_time: int
end_time: int
total_usd: float
currency: str = 'usd'
line_item_breakdown: dict[str, float]
project_breakdown: dict[str, float]
api_key_breakdown: dict[str, float]
raw_results: list
swarph_mesh.discovery.fetch_openai_cost_buckets(*, start_time, end_time=None, admin_key=None, group_by=None, limit=7, timeout=15.0)[source]

Hit OpenAI’s /v1/organization/costs for a date range.

start_time / end_time are Unix-seconds timestamps; bucket width is 1d (the only supported value as of 2026-05).

group_by accepts any subset of ["line_item", "project_id", "api_key_id"]. Default is no grouping (just total per bucket).

admin_key resolves from arg → $OPENAI_ADMIN_KEY env. If no key is found, returns [] (treat as “reconciliation not configured” — discovery.pricing primitives stay authoritative).

PRIVILEGE BOUNDARY: admin keys can mint more admin keys, delete keys, and manage org settings. swarph-mesh does NOT persist this key to disk — operators paste at daemon boot if reconciliation is wanted, and the env-var lives in the process for that session only. Treat this function as a privileged read; do NOT call it inside hot loops or chat paths.

Returns list[CostBucket] in chronological order (oldest first).

Parameters:
Return type:

list[CostBucket]

swarph_mesh.discovery.reconcile_openai_cost(*, start_time, end_time=None, swarph_attributed_usd=None, admin_key=None)[source]

Fetch OpenAI’s actual costs for a window + return a drift report against swarph-mesh’s attribution.jsonl-recorded total.

Returns dict:

{
    "openai_actual_usd": float,    # what OpenAI billed
    "swarph_attributed_usd": float | None,  # what we recorded
    "drift_usd": float | None,     # actual - attributed
    "drift_pct": float | None,     # (drift / attributed) * 100
    "buckets": list[CostBucket],   # per-day breakdown
    "window_start": int,           # unix seconds
    "window_end": int | None,
}

swarph_attributed_usd=None skips the comparison (just returns actual costs). Caller is responsible for summing the relevant attribution.jsonl rows for the same window.

Parameters:
  • start_time (int)

  • end_time (int | None)

  • swarph_attributed_usd (float | None)

  • admin_key (str | None)

Return type:

dict

swarph_mesh.discovery.pricing_for_gemini_model(model_hint, *, direction='output', tier_threshold_tokens=0, api_key=None, ttl_seconds=86400)[source]

Return USD-per-Mtok for a Gemini model+direction+tier combination.

model_hint is matched as a substring against SKU description’s model fragment (e.g., "1.5 Pro" matches "Gemini 1.5 Pro"). direction is "input" or "output". tier_threshold_tokens is 0 for base, 128000 for >128K.

Returns None when no matching SKU is found OR billing-API key is unavailable. Callers fall back to the adapter-local PRICING table (swarph_mesh.adapters.gemini.PRICING).

Parameters:
  • model_hint (str)

  • direction (str)

  • tier_threshold_tokens (int)

  • api_key (str | None)

  • ttl_seconds (int)

Return type:

float | None