Skip to content

provider-usage: add errorClass, a machine-readable reason for a degraded entry - #8

Merged
ualtinok merged 1 commit into
masterfrom
qta/error-class
Jul 28, 2026
Merged

provider-usage: add errorClass, a machine-readable reason for a degraded entry#8
ualtinok merged 1 commit into
masterfrom
qta/error-class

Conversation

@ualtinok

@ualtinok ualtinok commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

The gap

Every degraded entry on this wire looks alike. A provider nobody configured and a provider whose credential broke this morning both arrive as an entry with an error string, and the only thing separating them is that string — which is prose with no stability promise, and which the consumer contract explicitly tells consumers not to parse.

So a state change has no observable. A provider that worked yesterday and failed today moves a count by one and produces no other signal. On the current host: 3 genuinely failing providers hidden among 25 that are unconfigured and always will be.

ck quota renders exactly this today — "28 providers not connected" — and its filter is a presence check on usage, which is the right call against the wire as it stands. The gap is in the wire, not the renderer.

What this adds

errorClass: Option<String>, camelCase on the wire, additive and skip-if-none.

Value Meaning
credential_absent No credential found. Permanent and correct on a host that never configured this provider. Nothing to fix.
credential_unusable Found but unusable as it stands (empty, incomplete, refused by the credential store). Someone configured this and it needs fixing.
credential_rejected The upstream rejected it (401/403). Usually means logging in again.
no_quota_reported Credential works, account genuinely has no quota to report. Not a failure.
upstream_failed Could not reach the upstream, or an error status. Usually transient.
decode_failed Response arrived, wrong shape.

The producer derives the value from its error enum, never from message text, so it cannot drift from the taxonomy it names.

Why String and not an enum

This is deliberate, and it is the opposite of the type-first instinct.

The class list will grow. An enum makes an unrecognised value a parse failure, which on an observability surface means the entry disappears from the output at exactly the moment its state changed — the failure mode this field exists to fix, reintroduced one layer down. A string means a consumer meets a new class as data and prints it.

Consumers must render an unknown class as degraded-with-unknown-reason: never drop the entry, never fold it into an existing bucket. an_unknown_class_decodes_rather_than_failing pins that.

Compatibility

Additive and non-breaking:

  • Absent on healthy entries, omitted when unset.
  • A producer that never sets it serializes exactly as before — pinned by an_entry_without_a_class_serializes_as_it_did_before_the_field_existed.
  • degraded() is unchanged, so existing callers (including astrocyte's capacity_map fixture) compile untouched. degraded_with_class() is the new constructor for producers that know the class.

Version bumped 0.3.0 → 0.4.0.

Producer side

The producer taxonomy was made honest before this field was proposed, in ai-provider-quota 400b032 — around ten sites were reporting "credential present but broken" as absent, which would have filed broken credentials under nothing-to-do-here. That is the fail-open direction on an observability surface, so it had to be fixed first rather than after. error_class() there matches exhaustively over the error enum, so a new variant fails to compile rather than reaching consumers unclassified.

Producer-minted vectors (one entry per class, generated by serializing the real types rather than hand-written JSON) follow in the producer repo, so the consumer renderer is tested against what the producer actually emits.

Verification

cargo fmt --check clean, clippy -D warnings 0, 10/10 tests pass — 4 of them new, covering absence, round-trip, unknown-value tolerance, and healthy-entry absence.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Add errorClass to provider usage entries to give a stable, machine-readable reason for degraded entries. This makes real failures visible and distinct from unconfigured providers without breaking existing consumers.

  • New Features

    • Add optional errorClass (string) beside error on degraded entries; omitted on healthy/unset.
    • Values include: credential_absent, credential_unusable, credential_rejected, no_quota_reported, upstream_failed, decode_failed.
    • New constructor: degraded_with_class(...); degraded(...) unchanged.
    • Backward compatible: unknown classes decode as data (not parse errors). Bump cortexkit-provider-usage to 0.4.0.
  • Migration

    • Consumers: render unknown classes as “degraded (unknown reason)” and never drop entries.
    • Producers: use degraded_with_class when the class is known; otherwise continue with degraded.

Written for commit d20ec05. Summary will update on new commits.

Review in cubic

…ded entry

Every degraded entry looks alike on this wire. A provider nobody
configured and a provider whose credential broke this morning both
arrive as an entry with an error string, and the only thing telling
them apart is that string -- which is prose with no stability
promise, and which consumers are explicitly told not to parse.

The consequence is a state change with no observable: a provider
that worked yesterday and failed today moves a count by one and
produces no other signal. On the current host that is 3 genuinely
failing providers hidden among 25 that are unconfigured and always
will be.

errorClass carries the reason as a stable name derived from the
producer taxonomy rather than from the message text. Classes today:
credential_absent, credential_unusable, credential_rejected,
no_quota_reported, upstream_failed, decode_failed.

It is a String rather than an enum deliberately. The class list will
grow, and on an observability surface an unrecognised value must not
become a parse failure -- that would make a provider vanish from the
output at exactly the moment its state changed. Consumers render an
unknown class as degraded-with-unknown-reason, and a test pins that
an unknown value decodes intact.

Additive and non-breaking: absent on healthy entries, omitted when
unset, and a producer that never sets it serializes exactly as
before, which is also pinned by a test. degraded() is unchanged for
existing callers; degraded_with_class() is the constructor for
producers that know the class.
@ualtinok
ualtinok merged commit e132302 into master Jul 28, 2026
8 checks passed
@ualtinok
ualtinok deleted the qta/error-class branch July 28, 2026 21:05
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.

1 participant