Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d68d8e7
chore(openrouter): add @openrouter/agent and a temporary Responses probe
aliasunder Nov 11, 2026
a3d4727
refactor(openrouter): send review requests through the Agent SDK
aliasunder Nov 11, 2026
ff7cff2
docs: name the Agent SDK in the structure tree and remove the Respons…
aliasunder Nov 11, 2026
3f4a713
fix(review): list @openrouter/agent among the official SDKs in CONTRI…
aliasunder Nov 11, 2026
a011934
style: name the output ceiling maxOutputTokens and clarify the Agent …
aliasunder Nov 12, 2026
f521e87
test(openrouter): cover the routed model and tighten the Agent SDK cl…
aliasunder Nov 12, 2026
7636dcd
test(openrouter): assert whole attempts in cost tests and the cleared…
aliasunder Nov 12, 2026
8c178c5
fix(openrouter): describe where the Agent SDK gets the Responses result
aliasunder Nov 12, 2026
c4bd39e
refactor(openrouter): read the Responses result through the SDK's own…
aliasunder Nov 12, 2026
01401b3
test(orchestrate): say the client reads the stubbed Responses result
aliasunder Nov 12, 2026
419fda9
test(openrouter): name what the client stubs stand in for
aliasunder Nov 12, 2026
26f3f93
fix(openrouter): retry the status-200 error for a rejected non-stream…
aliasunder Nov 12, 2026
ed52c13
feat(openrouter): log a successful generation cost lookup
aliasunder Nov 12, 2026
c3cea6b
test(openrouter): pin the 408 retry and assert every retry test's ful…
aliasunder Nov 12, 2026
429000a
docs(openrouter): state the retry rule's one structural class in its …
aliasunder Nov 12, 2026
1f74eea
test(openrouter): build the overflow error from the captured body's w…
aliasunder Nov 12, 2026
ab65d28
refactor(openrouter): drop the generation cost lookup
aliasunder Nov 12, 2026
d68f14f
test: restore deadline coverage the cost lookup tests carried and nam…
aliasunder Nov 12, 2026
6349479
docs: list @openrouter/sdk with its test role in the SDK inventories
aliasunder Nov 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ src/
config.ts # action inputs → validated ActionConfig
logger.ts # structured JSON logger — levels, child contexts, lazy props
github/ # GitHub I/O: event payload → PrContext, octokit wrappers (diff fetch, review posting)
openrouter/ # OpenRouter I/O: @openrouter/sdk wrapper, per-attempt and shared review deadlines, retry ladder (up to two attempts per model, then the fallback_model input; on HTTP errors, timeouts, invalid structured output), cost summary
openrouter/ # OpenRouter I/O: review requests through @openrouter/agent, per-attempt and shared review deadlines, retry ladder (up to two attempts per model, then the fallback_model input; on HTTP errors, timeouts, invalid structured output), cost summary
diff/ # pure transforms over parse-diff output: git-quoted path decoding, annotation, commentable lines, diff-level exclusion (patterns, gitattributes linguist rules, wildcard cap: at most 2 "*" per path segment so matching cannot backtrack for minutes)
context/ # workspace I/O: conventions file, root .gitattributes, changed files, import-trace scan, doc-mention scan, priority docs
review/ # pure review logic (the model call is a function the caller passes in): finding schema, phases + stage dispatch (defined under Review instruction authoring), prompt, non-finding filter and unknown-file filter (README sections of those names), cross-phase merge, path normalization, selection, comment mapping, markdown code spans, title similarity, context notes, summary
Expand All @@ -47,7 +47,9 @@ backwards dependency and a bug — enforced by a
the outermost layer, so only tests import from a `__tests__/` folder.

Use the official SDKs — don't hand-roll what `@actions/core`,
`@actions/github` (octokit), `@openrouter/sdk`, or `parse-diff` already do.
`@actions/github` (octokit), `@openrouter/agent`, `@openrouter/sdk` (a dev
dependency: the tests take Responses result and error shapes from it), or
`parse-diff` already do.
Before writing any parsing or validation helper, check the SDK's utility
surface first: `@actions/core` ships `getBooleanInput` (strict YAML 1.2
booleans), which replaced a hand-rolled boolean-string Zod transform here.
Expand Down
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ points:
constructs SDK clients and touches `process.env`, except that `logger.ts`
reads `LOG_LEVEL`
- Use the official SDKs (`@actions/core`, `@actions/github`,
`@openrouter/sdk`, `parse-diff`) — don't hand-roll what they already do
`@openrouter/agent`, `@openrouter/sdk` for Responses result and error shapes
in tests, `parse-diff`) — don't hand-roll what they already do
- Relative imports carry explicit `.js` extensions (ESM runtime requirement)
- Tests are behavioral specs: one focused `it()` per behavior, exact
assertions, `const` per test
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ An empty value, such as an unset repo variable, selects the input's default, so
| `model` | `anthropic/claude-sonnet-4-6` | OpenRouter model slug exactly as listed on openrouter.ai/models |
| `fallback_model` | `""` | Model tried after the primary model's attempts fail (up to two per model). A timed-out primary attempt moves to this model immediately, without retrying the primary. Auth and credit errors (HTTP 401/402/403) stop the review without trying it. Empty = no fallback |
| `request_timeout_seconds` | `900` | Deadline for one model request, in seconds, capped by the time left in `review_timeout_seconds`. When it elapses, the attempt is recorded as `timeout` and the review moves to `fallback_model` if one is set; a timeout on the last model (the primary, when no fallback is set) may be retried once. The HTTP call is aborted best-effort: a request the provider keeps serving is still billed, and its cost-summary row shows `n/a`. Empty = 900 |
| `review_timeout_seconds` | `1500` | Shared model-work budget from action start, in seconds, including context preparation, all phases, retries, fallbacks and cost lookups. Completed phases post when it expires, with incomplete coverage identified; the run fails if none completed. Leave room below the workflow job timeout for setup and GitHub publication. External cancellation stops the run at once; unlike the deadline, it does not post completed phases. Empty = 1500 |
| `review_timeout_seconds` | `1500` | Shared model-work budget from action start, in seconds, including context preparation, all phases, retries and fallbacks. Completed phases post when it expires, with incomplete coverage identified; the run fails if none completed. Leave room below the workflow job timeout for setup and GitHub publication. External cancellation stops the run at once; unlike the deadline, it does not post completed phases. Empty = 1500 |
| `max_findings` | `""` _(uncapped)_ | Cap on posted findings, highest severity first. Empty = all validated findings post. |
| `severity_threshold` | `low` | Minimum severity to post: `low` \| `medium` \| `high` \| `critical` |
| `conventions_file` | `AGENTS.md` | Repo-relative path to the conventions file included in the prompt (truncated at `conventions_budget_tokens`). When the PR also changes the file, its full text is sent at most once, never twice |
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ inputs:
required: false
default: "900"
review_timeout_seconds:
description: Shared model-work budget from action start, in seconds, including context preparation, all phases, retries, fallbacks and cost lookups. Completed phases post when it expires, with incomplete coverage identified; the run fails if none completed. Leave room below the workflow job timeout for setup and GitHub publication. External cancellation stops the run at once; unlike the deadline, it does not post completed phases. Empty = 1500
description: Shared model-work budget from action start, in seconds, including context preparation, all phases, retries and fallbacks. Completed phases post when it expires, with incomplete coverage identified; the run fails if none completed. Leave room below the workflow job timeout for setup and GitHub publication. External cancellation stops the run at once; unlike the deadline, it does not post completed phases. Empty = 1500
required: false
default: "1500"
max_findings:
Expand Down
15 changes: 0 additions & 15 deletions fixtures/openrouter.chat-result.json

This file was deleted.

15 changes: 0 additions & 15 deletions fixtures/openrouter.invalid-json.json

This file was deleted.

168 changes: 168 additions & 0 deletions fixtures/openrouter.responses-accepted.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
{
"id": "gen-1794440085-n7DSIWy7fBrBUcqMEvla",
"object": "response",
"created_at": 1794440085,
"model": "z-ai/glm-5.3-flash",
"status": "completed",
"completed_at": 1794440086,
"output": [
{
"id": "rs_tmp_viau8k9rg1",
"type": "reasoning",
"status": "completed",
"content": [
{
"type": "reasoning_text",
"text": "User asks to report no findings. Provide JSON with analysis and empty findings array."
}
],
"summary": [],
"format": "unknown"
},
{
"id": "msg_tmp_o67rrerum0m",
"type": "message",
"role": "assistant",
"status": "completed",
"content": [
{
"type": "output_text",
"text": "{\n \"analysis\": \"Reviewed the single-line change `export const greeting = \\\"hello\\\"`. This is a simple, valid module-level constant export. The syntax is correct, the value is a static string with no side effects, and naming follows common conventions. No security, correctness, style, or maintainability concerns were identified. As requested, there are no findings to report.\",\n \"findings\": []\n}",
"annotations": [],
"logprobs": []
}
]
}
],
"error": null,
"incomplete_details": null,
"access_programs": null,
"tools": [],
"tool_choice": "auto",
"parallel_tool_calls": true,
"max_output_tokens": 128000,
"temperature": 1,
"top_p": 1,
"presence_penalty": 0,
"frequency_penalty": 0,
"top_logprobs": 0,
"max_tool_calls": null,
"metadata": {},
"background": false,
"previous_response_id": null,
"service_tier": "auto",
"truncation": "disabled",
"store": false,
"instructions": "You are a code reviewer. Reply with JSON that matches the schema: an analysis string and a findings array.",
"text": {
"format": {
"type": "json_schema",
"name": "review_response",
"strict": true,
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"analysis": {
"type": "string"
},
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"file": {
"type": "string",
"minLength": 1
},
"line": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
},
"end_line": {
"anyOf": [
{
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
},
{
"type": "null"
}
]
},
"category": {
"type": "string",
"enum": ["correctness", "security", "conventions", "tests", "subtle_bugs", "ci"]
},
"severity": {
"type": "string",
"enum": ["critical", "high", "medium", "low"]
},
"confidence": {
"type": "string",
"enum": ["high", "medium", "low"]
},
"title": {
"type": "string",
"minLength": 1
},
"description": {
"type": "string",
"minLength": 1
},
"suggestion": {
"type": ["string", "null"]
},
"failure_scenario": {
"type": "string",
"minLength": 1
}
},
"required": [
"file",
"line",
"end_line",
"category",
"severity",
"confidence",
"title",
"description",
"suggestion",
"failure_scenario"
],
"additionalProperties": false
}
}
},
"required": ["analysis", "findings"],
"additionalProperties": false
}
}
},
"reasoning": {
"effort": "max"
},
"safety_identifier": null,
"prompt_cache_key": null,
"prompt_cache_options": null,
"usage": {
"input_tokens": 54,
"input_tokens_details": {
"cached_tokens": 0
},
"output_tokens": 102,
"output_tokens_details": {
"reasoning_tokens": 17
},
"total_tokens": 156,
"cost": 0.000021036,
"is_byok": false,
"cost_details": {
"upstream_inference_cost": 0.000021036,
"upstream_inference_input_cost": 0.000002268,
"upstream_inference_output_cost": 0.000018768
}
}
}
6 changes: 6 additions & 0 deletions fixtures/openrouter.responses-bad-key-body.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"error": {
"message": "User not found.",
"code": 401
}
}
Loading
Loading