You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(openrouter): add response validation and harden error handling (#81)
* feat(openrouter): add runtime response validation at service boundary
Add three TypeScript assertion functions that validate OpenRouter API
responses before they propagate to consumers, replacing silent type
casts with explicit runtime checks:
- validateModelsResponse(): ensures .data is an array with .id (string)
and .pricing.prompt/.completion (strings) on each model
- validateChatResponse(): ensures .choices is an array and .usage
(if present) has numeric token fields
- validateStreamChunk(): ensures .usage (if present) has numeric
prompt_tokens, completion_tokens, total_tokens fields
All three validators throw OpenRouterError(message, 502) on failure so
callers get a clear upstream error rather than undefined access issues.
Validators are applied immediately after each response.json() cast in
getModels(), createChatCompletion(), and createUsageCapturingStream().
Stream chunk validation failures are caught and logged as warnings to
avoid breaking active streams.
OpenRouterError class moved above validators to resolve declaration
ordering (validators reference the class in their throw statements).
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(openrouter): guard response.data and model.pricing in list-models endpoint
Add belt-and-suspenders Array.isArray check on response.data and per-model
pricing type guards inside the .map() callback. Invalid models are filtered out
(partial data) instead of crashing the endpoint, making the listing resilient
to any future validator contract changes.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(openrouter): validate non-empty choices before returning chat completion
Guard response.choices with Array.isArray and length > 0 check after the service
call. An empty choices array from OpenRouter now returns a structured 502 error
instead of passing through a useless response. Also logs a warning when the first
choice has unexpectedly empty content.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(model-cache): add per-model pricing guards in doRefresh cache population
Guard modelsResponse.data with Array.isArray before iterating, and add explicit
type checks on each model.pricing before parseFloat(). A single malformed model
now skips with a debug log rather than aborting the entire cache refresh. This
makes the cache resilient to future validator contract changes.
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(model-cache): add getCacheStatus, getSimilarModels, and degraded flag
Add getCacheStatus() to expose observable cache state (warm/cold/degraded)
so callers and operators can distinguish between a healthy cache, a never-
populated cold start, and a failed-fetch degraded state.
Add getSimilarModels() for use in invalid-model error responses — returns up
to N model IDs from the registry that share a provider prefix with the
requested model, so agents get actionable correction hints.
Update ModelLookupResult to carry an optional degraded flag when the registry
is empty after a refresh attempt, allowing consumers to log and surface the
degraded state rather than silently allowing the request.
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(chat): add post-payment model validation with similar model suggestions
The x402 middleware rejects invalid models pre-payment, but when the cache
is cold/degraded at middleware time the check is skipped. Adding a second
lookupModel() call in the chat handler catches this case once the cache has
been populated, returning a 400 with code: "invalid_model", the rejected
model ID, and up to 3 provider-prefix-matched suggestions from the live
registry so clients get actionable correction hints.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(x402): include model ID in invalid_model rejection and log degraded state
The pre-payment model rejection now includes the model field alongside the
error message and code so clients and logs always have the rejected model ID
without having to reconstruct it from the request body.
When lookupModel returns valid:true/degraded:true the middleware now logs a
warning instead of silently passing through, giving operators visibility when
the model registry cache was unavailable at validation time.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(openrouter): add unit tests for response validation helpers
Adds tests/openrouter-validation.unit.test.ts covering all three
validator functions (validateModelsResponse, validateChatResponse,
validateStreamChunk). Tests verify both happy paths and every error
branch, ensuring the service-boundary validators reject malformed
OpenRouter responses with OpenRouterError status 502.
Closes#80
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(openrouter): simplify validation code and remove redundant guards
Extract shared usage validation helper, remove belt-and-suspenders guards
that duplicate Phase 1 validator guarantees, and reduce test boilerplate
with a shared assertion helper. Net -138 lines, same coverage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR review feedback from Copilot and Arc
- Add null/non-object element guard in validateModelsResponse
- Rewrite expectOpenRouterError to invoke fn once (not twice)
- Add null/non-object element test cases for models validator
- Remove redundant per-model pricing guard in doRefresh (strict validator handles it)
- Add clarifying comment on getSimilarModels fallback behavior
- Add unit tests for getCacheStatus and getSimilarModels via test helpers
- Export _seedCacheForTesting/_resetCacheForTesting for test isolation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
0 commit comments