Skip to content

refactor(openrouter): send review requests through the Agent SDK - #143

Draft
aliasunder wants to merge 19 commits into
mainfrom
worktree-agent-sdk-migration-46d6bf54
Draft

aliasunder wants to merge 19 commits into
mainfrom
worktree-agent-sdk-migration-46d6bf54

Conversation

@aliasunder

@aliasunder aliasunder commented Nov 11, 2026 •

Copy link
Copy Markdown
Owner

Summary

Moves the review request from @openrouter/sdk's chat.send to callModel from @openrouter/agent, one streaming Responses API request per attempt with structured output through text.format. The retry ladder keeps its attempt counts, timeout advance, fitted-ceiling retry and key rejection, with one change to what counts as structural: only a 4xx outside 408 and 429 skips the same-model retry now, so the status-200 error the SDK throws when a non-streamed body fails its result schema retries once like a network error instead of advancing straight to the fallback.

The generation cost lookup is gone: OpenRouter includes the cost in every response's usage block, the probe found it on every slug, and no run has shown it missing, so an accepted attempt records the cost it carries and a missing one stays null in the cost table. @openrouter/sdk moves to the dev dependencies, where the tests use its result transform and error classes.

A temporary probe workflow on this branch captured raw Responses API payloads with the repo's key: an accepted payload for every model slug the consumers run that OpenRouter still lists, a forced context overflow, and a bad key. The captures drive the client's tests as fixtures, run through the SDK's own result transform, and the error fixtures build the SDK's error classes. The probe files were removed once the captures landed.

What the probe showed

  • An accepted payload's response.completed event carries the message text and a numeric usage.cost; the SDK's getText() extracts the text.
  • A context overflow is a 400 whose window and input counts sit in error.message, worded as on the chat path, so the fitted-ceiling retry reads the same pattern.
  • A bad key is a 401 from the SDK's error class, so key rejection stops the ladder as before.
  • Two consumer slugs failed on a model OpenRouter no longer lists; that is a consumer variable, not a Responses-path difference.

The first consumer bump that runs GLM as its primary is the live check for that model; the self-review here runs DeepSeek.

Planted Finding

Head: 6349479

Plant PR #147, cut from this head, made the client never mark an attempt as key-rejected, so a 401, 402 or 403 would go on to the fallback model with the same dead key. The bot built from that branch filed it as a high-severity correctness finding, "Mark 401/402/403 attempts key-rejected so the ladder aborts": #147 (comment). The comment names the planted rule.

Three earlier plants on earlier heads were caught the same way: the same key-rejection plant from ab65d28 (#146 (comment)), and a retry on every 4xx from 8c178c5 (#145 (comment)) and from ff7cff2 (#144 (comment)).

🤖 Generated with Claude Code

The probe workflow captures the raw accepted, context-overflow and bad-key
payloads from the Responses API for every slug the consumers run, so the
client's new fixtures come from the wire rather than from guesses. Both
probe files are removed before merge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@socket-security

socket-security Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​openrouter/​agent@​1.1.01001001009780

View full report

Comment thread .github/workflows/probe_responses.yml Resolved
Comment thread .github/scripts/probe-responses.ts Resolved
Comment thread .github/scripts/probe-responses.ts Resolved
Comment thread .github/scripts/probe-responses.ts Resolved
Comment thread .github/workflows/probe_responses.yml Resolved
@umm-actually

umm-actually Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

umm-actually re-reviewed at 6349479

1 new finding(s) posted (17 tracked finding(s) across all runs).

Context notes
  • 1 changed file(s) excluded from review: package-lock.json (built-in default list)

umm-actually · deepseek/deepseek-v4.1-flash

aliasunder and others added 2 commits November 11, 2026 18:48
The review client calls callModel on @openrouter/agent, one streaming
Responses API request per attempt with structured output through
text.format, and reads the text the SDK extracts. The retry ladder keeps
its policy; @openrouter/sdk stays for the generation cost lookup. Tests
run on raw Responses payloads captured from live requests, transformed
by the SDK's own public helper, and build the SDK's error classes from
captured error bodies.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…es probe

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread src/openrouter/client.ts Resolved
Comment thread src/openrouter/client.ts Resolved
@umm-actually

umm-actually Bot commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

Add @openrouter/agent to CONTRIBUTING's official SDK list
Low severity · conventions · high confidence

CONTRIBUTING.md:45 — beyond the diff's line ranges, in code the changes touch or depend on.

This PR adds @openrouter/agent to package.json and swept the identical SDK list in AGENTS.md, but the contributor-facing copy in CONTRIBUTING.md still names only @openrouter/sdk, so the doc that tells contributors which SDKs to reach for understates what the review path uses.

Failure scenario: A contributor adding another OpenRouter call reads CONTRIBUTING.md's list, does not see the agent package, and hand-rolls a fetch or reuses the REST client for the review request; the review path then carries two request implementations that drift apart on retry, timeout and structured-output handling.

Suggested fix
- Use the official SDKs (`@actions/core`, `@actions/github`,
  `@openrouter/agent`, `@openrouter/sdk`, `parse-diff`) — don't hand-roll what they already do

umm-actually · deepseek/deepseek-v4.1-flash

aliasunder and others added 5 commits November 11, 2026 18:58
…BUTING.md

AGENTS.md names the Agent SDK in its official-SDK list; the contributor
guide's copy of that list now matches.

Ship-Check: pr-review · claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…SDK client

- Rename the output-ceiling identifiers to the Responses field's name
  (MAX_OUTPUT_TOKENS, MIN_FITTED_MAX_OUTPUT_TOKENS, fittedMaxOutputTokens,
  the maxOutputTokens local and its fitted-retry log field)
- Rename createOpenRouterClient's sdk param to costLookupClient
- Name @openrouter/agent and its bundled @openrouter/sdk copy where the
  client relies on them; state where callModel takes its abort signal,
  why getText sends no second request, and that a Responses id is the
  generation id the cost lookup takes
- Map requestReview's loop exits; give the 128_000 ceiling its reason
- main.ts: say why unhandled rejections only warn and why cleanups are a Set
- Docs: drop API detail from the AGENTS.md structure line; give each
  OpenRouter SDK its role in CONTRIBUTING.md

Ship-Check: code-quality · claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ient tests

- Assert modelUsed comes from the response's model with a requested
  model that differs from it; every other test requested the model the
  fixture reports, so returning the requested model passed them all.
- Drop the per-fixture transform cases: the module-scope transform throws
  at collection first, so those cases could never be the failing test.
- Assert the SDK-thrown TimeoutError test's calls in one positional
  toEqual over model, signal and retry options.
- Rename sdkWithThrowingLookup to throwingCostLookupClient.

Ship-Check: test-audit · claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… timer

- The cost lookup tests assert the full accepted attempt instead of
  picking costUsd off the first one.
- The timer test asserts no timer is left pending under fake timers; a
  clearTimeout spy passed on any clearTimeout call anywhere.

Ship-Check: test-audit · claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SDK takes the result from the stream's completion event rather than
assembling it from the streamed events, and the generation cost lookup is
the one endpoint this client calls that the Agent SDK does not cover.

Ship-Check: bug-check · claude-opus-5-5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aliasunder

Copy link
Copy Markdown
Owner Author

Fixed in 3f4a713: CONTRIBUTING.md's official SDK list names @openrouter/agent beside @openrouter/sdk, with each package's role.


🔍 ship-check · pr-monitor · claude-fable-5-1

aliasunder and others added 2 commits November 11, 2026 19:46
… validation

The Agent SDK validates the completion event before getResponse resolves,
so the client reads id, model and usage through a structural type instead
of parsing them a second time. The test stub for an unrecognized shape,
which production cannot produce, goes with the branch it exercised.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The client no longer parses the result; the stub comment now matches.

Ship-Check: test-audit · claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/openrouter/__tests__/client.test.ts Resolved
Comment thread src/openrouter/__tests__/client.test.ts Resolved
@umm-actually

umm-actually Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Restate 'one request per review phase' for the agent run
Low severity · subtle bugs · medium confidence

README.md:117 — beyond the diff's line ranges, in code the changes touch or depend on.

The 'one request per review phase' mechanism claim predates the Agent SDK: a phase is now one callModel run, which may issue additional billed requests (the SDK's empty-final retry / final turn), so the sentence misstates the request and cost unit a phase represents.

Failure scenario: An operator reading a parallel run's cost table expects exactly one billed request per phase; a phase whose response comes back empty makes the SDK issue a second request whose tokens and cost never appear in the summary, and the doc's one-request framing leaves no way to notice the gap.

Suggested fix
State the run granularity, e.g.: "Sends one model call per review phase to OpenRouter (see the `phases` input for dispatch modes); the SDK may issue additional requests inside a call, such as its empty-final retry."

umm-actually · deepseek/deepseek-v4.1-flash

aliasunder and others added 5 commits November 11, 2026 19:53
The text extractor and the status errors in the client tests mirror the
Agent SDK's bundled copy rather than calling it, so the comments say which
contract each test pins, and the empty-content case is named for the
mapping it asserts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ed body

The Agent SDK accepts a non-streamed JSON body when the API answers without a stream, and throws a status-200 error when that body fails its result schema. The retry rule treated every status outside 5xx, 408 and 429 as structural, so that error moved straight to the fallback model, while the same malformed result over a stream fails status-less and retries. Only a 4xx other than 408 or 429 is structural now.

Ship-Check: bug-check · claude-opus-5-5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An accepted response without usage.cost sends the client to the generation
endpoint, and nothing recorded how often that happens. The info line gives
the run logs the rate that decides whether the fallback stays.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…l attempt list

The 200-body retry test now throws the SDK's own ResponseValidationError.
The 408 status shares the 429 retry test, and the index-picked attempt
assertions in the retry and truncation tests now assert the whole list.

Ship-Check: test-audit · claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…constant's doc

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread src/openrouter/__tests__/client.test.ts Resolved
Comment thread src/openrouter/client.ts Resolved
@umm-actually

umm-actually Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Strip PR numbers, SHAs and review chronology from the PR description
Low severity · conventions · medium confidence

AGENTS.md:344 — beyond the diff's line ranges, in code the changes touch or depend on.

The PR description carries internal references — PR #145 and #144, the commit SHAs 8c178c5 and ff7cff2, links to prior review-bot discussion comments, and the chronology of an earlier planted finding — which AGENTS.md forbids in public artifacts, PR descriptions included. Those references are permanently visible to anyone reading the public repository, while the description never explains the SDK migration a future reader needs.

Failure scenario: A reader browsing the public repository opens this PR and learns which evaluation PRs planted which bugs, the commits they were cut from, and the bot discussion links used to score them, while someone later hunting the rationale for the SDK migration finds only experiment chronology in the PR body.

Suggested fix
Keep the description to the change itself: the review request moving to `@openrouter/agent`'s `callModel` (Responses API, `text.format` structured output), `@openrouter/sdk` staying for the generation cost lookup, the retry ladder it preserves, and the fixtures the captures became. Move the plant/verification narrative to the harness or issue tracker entries instead of the PR body.

umm-actually · deepseek/deepseek-v4.1-flash

aliasunder and others added 3 commits November 11, 2026 20:08
…ording

The fitted-retry tests now run on the live overflow message with only its
numbers replaced, so a reworded rejection fails the suite instead of
silently disabling the retry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
OpenRouter includes the cost in every response's usage block, and no run
has shown it missing, so the accepted attempt records the cost it carries
and a missing one stays null. The lookup, its second client and the runtime
@openrouter/sdk dependency go with it; the package stays a dev dependency
for the test fixtures' transform and error classes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e the null-cost case

- client: an accepted response that spends the review deadline is still returned
- orchestrate: a deadline spent after every phase completed publishes full coverage with no deadline warning
- rename the no-usage-block test so its name covers the null cost it asserts

Ship-Check: test-audit · claude-opus-5-5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/openrouter/client.ts Resolved
Comment thread AGENTS.md Resolved
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread src/openrouter/client.ts Resolved
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