Repository navigation
Conversation
🦋 Changeset detectedLatest commit: cbf9e8d The changes in this PR will be included in the next version bump. This PR includes changesets to release 51 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
|
Thanks for the PR, @ktrz! 🙌 @AlemTuzlak will take a look. Automated pre-review checks
Automated triage — a human review follows. |
ea60d7e to
8119f5e
Compare
- Add EvaluateError, a core error class an evaluate adapter can throw when the provider answered but the call cannot become a result - It carries a stable code plus the usage, model and raw wire answers the provider already returned - Set name to EvaluateError in the constructor, as SkillLimitError does - Add EVALUATE_REFUSAL_CODE as the code for a refused question - Export both and the EvaluateErrorInit type from @tanstack/ai and @tanstack/ai/adapters - Add a unit test for the fields, name and code
- When the adapter throws an EvaluateError that carries usage, decide() reports it through the evaluate:usage event and onUsage, with the same payload as a successful call - Detect the error with instanceof EvaluateError - Then run onError or onAbort as before and rethrow the original error - onFinish does not run on failure, and the usage is reported once - Run onError or onAbort in a finally block, so a throwing onUsage hook cannot leave the call without a terminal hook - Document the behaviour in the decide() JSDoc - Add tests for the usage report and hook order, a throwing onUsage hook, and an EvaluateError without usage
- A refused question now throws EvaluateError with code EVALUATE_REFUSAL_CODE instead of a plain Error - The error carries the usage, the model and the wire answers that did come back, so the refused question is the missing key - The message text is the same as before, and names the first refused question - Map all answers before throwing, so a refusal no longer drops the other answers and the usage - Extend the refusal unit test to check the class, code, message, usage, model and answers - Add a test for two refusals in one call - Add a minor changeset for @tanstack/ai and @tanstack/ai-openai covering the new error, its exports, the refusal behaviour and the usage reporting in decide()
- Add a Refused questions section to the evaluate guide with an instanceof EvaluateError sample that rethrows other errors - List the usage, model and answers fields the error keeps, and show that answers are raw wire answers - Say that decide() reports the usage through onUsage, then runs onError and rethrows - Point the OpenAI adapter note at the new section - Bump updatedAt for both pages in docs/config.json
- Mount /evaluate-refusal in aimock to answer /v1/decisions with a refusal answer and billed usage - Add /api/evaluate-refusal, which runs decide() with the OpenAI decider against that mount and returns the rejection as name, code, message and usage - Record the usage seen by onUsage middleware - Add evaluate-refusal.spec.ts asserting an EvaluateError with code refusal and the usage on the error and in onUsage
8119f5e to
cbf9e8d
Compare
When OpenAI refuses to answer a
decide()question, the adapter now throws anEvaluateErrorwithcode: EVALUATE_REFUSAL_CODE, the billedusage, themodel, and the rawanswersthat did come back. Before, it threw a plainErrorand all of that was lost.decide()reports the usage of anyEvaluateErrorthroughonUsageandevaluate:usagebeforeonErrorruns, so a refused call is no longer missing from usage metering. The message text does not change.🎯 Changes
@tanstack/ai:EvaluateError,EvaluateErrorInitandEVALUATE_REFUSAL_CODE = 'refusal'inevaluate/adapter.ts, exported from the root and from@tanstack/ai/adapters. MirrorsSkillLimitError(small core class) andCONTEXT_OVERFLOW_CODE(string constant, plainstringcode), the same shape feat(ai, openai-base, ai-anthropic): put retryAfterMs on RUN_ERROR #1678 and feat(ai, openai-base, adapters): a stable context_overflow code on RUN_ERROR #1739 used for stable codes onRUN_ERROR.isEvaluateError(value): value is EvaluateError, exported from both entry points. It checksname === 'EvaluateError'and a stringcode, notinstanceof, so it works when the app has two copies of@tanstack/ai. Same pattern asisMCPInputRequiredErrorinpackages/ai-mcp/src/input-required.ts.decide()catch: ifisEvaluateError(error)and it hasusage, emitevaluate:usageand runonUsage, then runonAbortoronErrorin afinally, then rethrow.onFinishdoes not run.@tanstack/ai-openai:toWireAnswerreturnsundefinedfor a refusal, the adapter collects the answered keys, and throwsEvaluateErrorafter the map. With more than one refusal, the message names the first. The adapter log entry now hascode: 'refusal'.packages/ai/tests/evaluate.test.ts, six inevaluate-error.test.ts(one for the class, five for the guard), two inpackages/ai-openai/tests/evaluate-adapter.test.ts.docs/evaluate/evaluate.md, refusal bullet indocs/adapters/openai.md. Changeset: minor for both packages.testing/e2e/tests/evaluate-refusal.spec.tsrunsdecide()with the OpenAI decider against an aimock route that returns a refusal. It checks for anEvaluateErrorwith coderefusaland the usage, and thatonUsagesaw the usage once.answersholds raw wire answers (aboolean()answer is{ type: 'noul', noul }, achoice()answer haschoice), because that is what the adapter has. The refused question has no key. If you would rather not expose the wire shape, I can dropanswersand keepcode,usageandmodel.This is the typed half of #1741. A separate PR fixes the untyped half, where
decide()drops usage when answer mapping fails.✅ Checklist
pnpm run test:pr, or these tests do not apply to this pull request.docs/for this change, or this change is not user-facing.pnpm changeset), or this PR does not change a published package.🚀 Release Impact
Testing
Commands run. All with
--skip-nx-cache.pnpm nx run-many -t test:lib test:types test:oxlint -p @tanstack/ai @tanstack/ai-openai: green.pnpm test:pr: exit 0.pnpm nx affectedwith thetest:prtarget list, plustest:lib test:build build: green.pnpm test:docs: no broken links.oxfmt --check .: clean.pnpm exec playwright test tests/evaluate-refusal.spec.tsintesting/e2e: green. On main it fails because the error is a plainErrorwith nocodeorusage.Manual test.
pnpm nx run @tanstack/ai-openai:test:lib --skip-nx-cache -- tests/evaluate-adapter.test.tswith this branch's test file.throws an EvaluateError with usage and the other answers when a question is refusedfails, along withnames the first refused question and leaves out every refused key, becauseEvaluateErrordoes not exist on main.pnpm nx run @tanstack/ai:test:lib --skip-nx-cache -- tests/evaluate.test.tswith this branch's test file.reports the usage of an EvaluateError once, then fires onError and rethrows it unchangedfails, along with the three other newEvaluateErrortests, for the same reason.instanceof EvaluateErrorput back in thedecide()catch,reports the usage of an EvaluateError from another copy of the packagefails. With the guard it passes.How this PR makes testing easy. The unit tests above. The OpenAI tests use recorded Decisions responses with one answered and one or two refused questions, so no API key is needed.
tests/evaluate-error.test.tscovers the guard: a caughtEvaluateError, an error from another copy of the class, a plain object with the same shape, and rejections for a plainError, non-objects, a missing or numericcode, and a wrongname.Linked issues
Part of #1741
Risk / rollback
Errorwith the same message. Itsnameis now'EvaluateError'instead of'Error', which shows in logs and in the OpenTelemetryerror.typeattribute.onUsagecan now run on a faileddecide()call. Middleware that assumedonUsagemeans success will see it beforeonError. The exactly-one-terminal-hook rule still holds, becauseonFinishdoes not run.onUsagehook throws whiledecide()reports anEvaluateError,onErrorstill runs with the original error (that is what thefinallyis for), and the hook's error propagates instead of the original. This matches how a throwing hook behaves elsewhere.decide()and the docs useisEvaluateError(), so detection survives two installed copies of@tanstack/ai.instanceof EvaluateErrorstill works for single-copy users.Rollback: revert the PR. No data or config to undo.
Public API change
Before
After
Summary by CodeRabbit