Review this repo's PRs via OpenRouter in a single step #75

Manually merged
marvin8 merged 8 commits from feat/issue-74-openrouter-own-review into main 2026-10-15 20:15:11 +00:00
Collaborator

The pr-review job now reviews every PR in a single WuMing invocation through OpenRouter instead of two invocations against Moonshot and DeepSeek:

  • action.yml exposes the four backend inputs added in #73 (openrouter_key, openai_base_url, openai_key, openai_model) and updates the backend description to list all six values
  • ci.yml merges the Kimi and DeepSeek review steps into one OpenRouter run: code/security stay on Kimi K2.7 Code (moonshotai/kimi-k2.7-code); config/docs/shell/tests move to DeepSeek V4.1 Flash (deepseek/deepseek-v4.1-flash) — the same DeepSeek flash line, one version newer than the previous deepseek-v4-flash (slugs verified against the live OpenRouter catalog)
  • Hosted backends (deepseek, moonshot, openrouter) now fail fast at startup when their API key is empty; the generic openai backend still allows keyless local servers
  • Both deployment guides (Forgejo Actions, Woodpecker) gain OpenRouter and generic-openai backend sections, and the max-tokens note now covers all OpenAI-compatible backends

Closes #74

The `pr-review` job now reviews every PR in a single WuMing invocation through OpenRouter instead of two invocations against Moonshot and DeepSeek: - `action.yml` exposes the four backend inputs added in #73 (`openrouter_key`, `openai_base_url`, `openai_key`, `openai_model`) and updates the `backend` description to list all six values - `ci.yml` merges the Kimi and DeepSeek review steps into one OpenRouter run: `code`/`security` stay on Kimi K2.7 Code (`moonshotai/kimi-k2.7-code`); `config`/`docs`/`shell`/`tests` move to DeepSeek V4.1 Flash (`deepseek/deepseek-v4.1-flash`) — the same DeepSeek flash line, one version newer than the previous `deepseek-v4-flash` (slugs verified against the live OpenRouter catalog) - Hosted backends (`deepseek`, `moonshot`, `openrouter`) now fail fast at startup when their API key is empty; the generic `openai` backend still allows keyless local servers - Both deployment guides (Forgejo Actions, Woodpecker) gain OpenRouter and generic-openai backend sections, and the max-tokens note now covers all OpenAI-compatible backends Closes #74
📝 document OpenRouter and generic openai backend setup in the Actions guides
All checks were successful
/ gitleaks (pull_request) Successful in 14s
/ checks (pull_request) Successful in 1m58s
/ deploy-docs (pull_request) Has been skipped
/ publish-container (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 2m1s
54e51d3d8b
forgejo-actions left a comment

WuMing

Found 5 issue(s). See inline comments below.

## WuMing Found **5** issue(s). See inline comments below.
@ -20,2 +20,2 @@
backend: moonshot
moonshot_key: ${{ secrets.wuming_moonshot_key }}
backend: openrouter
openrouter_key: ${{ secrets.wuming_openrouter_key }}

config [MEDIUM]

The job now depends on a repo secret wuming_openrouter_key that does not exist yet (per the PR note). Because openrouter_key in action.yml is declared required: false with default: "", a missing secret is not rejected up front — the action runs with an empty key and fails deep inside the review, producing a confusing error. Create the secret before merging (or add an explicit guard step that fails fast when the key is empty).

**config** [MEDIUM] The job now depends on a repo secret `wuming_openrouter_key` that does not exist yet (per the PR note). Because `openrouter_key` in action.yml is declared `required: false` with `default: ""`, a missing secret is not rejected up front — the action runs with an empty key and fails deep inside the review, producing a confusing error. Create the secret before merging (or add an explicit guard step that fails fast when the key is empty). <!-- wuming:sha256:71caab251930e628ba9c3bea059925e21d7ecd4ab5103a13d8016811f3c4fafb -->
marvin8 marked this conversation as resolved
@ -29,3 +23,1 @@
deepseek_key: ${{ secrets.wuming_deepseek_key }}
pr_number: ${{ github.event.pull_request.number }}
agents: config:deepseek-v4-flash,docs:deepseek-v4-flash,shell:deepseek-v4-flash,tests:deepseek-v4-flash
agents: code:moonshotai/kimi-k2.7-code,security:moonshotai/kimi-k2.7-code,config:deepseek/deepseek-v4.1-flash,docs:deepseek/deepseek-v4.1-flash,shell:deepseek/deepseek-v4.1-flash,tests:deepseek/deepseek-v4.1-flash

config [MEDIUM]

The model assignments are not actually preserved: the four config/docs/shell/tests agents previously ran deepseek-v4-flash, but now use deepseek/deepseek-v4.1-flash — a different model version, not just an OpenRouter vendor-namespaced form of the old slug. This silently changes review behaviour (and will hard-fail the whole job if the slug does not exist in the OpenRouter catalog). If the intent was a pure migration, use the vendor-prefixed slug of the existing model (e.g. deepseek/deepseek-v4-flash); if the upgrade is deliberate, call it out explicitly rather than describing it as 'preserving the model assignments'.

**config** [MEDIUM] The model assignments are not actually preserved: the four config/docs/shell/tests agents previously ran `deepseek-v4-flash`, but now use `deepseek/deepseek-v4.1-flash` — a different model version, not just an OpenRouter vendor-namespaced form of the old slug. This silently changes review behaviour (and will hard-fail the whole job if the slug does not exist in the OpenRouter catalog). If the intent was a pure migration, use the vendor-prefixed slug of the existing model (e.g. `deepseek/deepseek-v4-flash`); if the upgrade is deliberate, call it out explicitly rather than describing it as 'preserving the model assignments'. <!-- wuming:sha256:709a62f902cfa1b3e40133c459fceeed5b36181debb03c01518ef2a6c8dc17ee -->
marvin8 marked this conversation as resolved
action.yml Outdated
@ -52,2 +66,4 @@
required: false
default: ""
agents:
description: "Comma-separated list of agents, e.g. code:deepseek-v4-flash,security. Valid names: code, config, docs, performance, security, shell, tests."

config [LOW]

The agents input documentation was not updated for the new backends: it still shows only a bare model name (code:deepseek-v4-flash) and lists valid agent names, but never explains that OpenRouter requires full vendor/model slugs (e.g. moonshotai/kimi-k2.7-code). The new openrouter_key description refers to 'OpenRouter vendor/model slugs' without saying where they go. Document the agent:vendor/model format (and per-backend model naming) in this description so users of the six documented backends know what to pass.

**config** [LOW] The `agents` input documentation was not updated for the new backends: it still shows only a bare model name (`code:deepseek-v4-flash`) and lists valid agent names, but never explains that OpenRouter requires full `vendor/model` slugs (e.g. `moonshotai/kimi-k2.7-code`). The new `openrouter_key` description refers to 'OpenRouter vendor/model slugs' without saying where they go. Document the `agent:vendor/model` format (and per-backend model naming) in this description so users of the six documented backends know what to pass. <!-- wuming:sha256:dbf44e11512972526120c6f92e296b09b63ba7804f385afd6a6eb2fa04726354 -->
marvin8 marked this conversation as resolved
action.yml Outdated
@ -52,0 +54,4 @@
required: false
default: ""
openai_base_url:
description: Base URL of any OpenAI-compatible endpoint (e.g. https://openrouter.ai/api/v1, http://localhost:1234/v1). Required when backend=openai.

security [LOW]

A02: The OpenAI-compatible base URL example includes an unencrypted HTTP endpoint (http://localhost:1234/v1). Since openai_key will be sent to this URL, documenting an HTTP example risks API keys being transmitted in plaintext. Remove the HTTP example and only show HTTPS endpoints, or add explicit guidance that HTTP must not be used for remote/production endpoints.

**security** [LOW] A02: The OpenAI-compatible base URL example includes an unencrypted HTTP endpoint (http://localhost:1234/v1). Since openai_key will be sent to this URL, documenting an HTTP example risks API keys being transmitted in plaintext. Remove the HTTP example and only show HTTPS endpoints, or add explicit guidance that HTTP must not be used for remote/production endpoints. <!-- wuming:sha256:23aaac9ec2d9d99653fe39e62126a33990ac7597bc719d950d26a7a7b3347760 -->
marvin8 marked this conversation as resolved
@ -45,3 +45,3 @@
## Choosing a backend
WuMing supports four AI backends. Set `WUMING_BACKEND` to select one.
WuMing supports six AI backends. Set `WUMING_BACKEND` to select one.

docs [MEDIUM]

The backend count was bumped from four to six, but the "Choosing a backend" section that follows only documents four backends: DeepSeek (line 49), Kimi/Moonshot (ends line 102), OpenRouter (line 104) and the generic OpenAI-compatible endpoint (line 120). The max-tokens note further down (lines 160-162) also only names those same four as OpenAI-compatible, so readers are told there are six backends but can never discover the remaining two. Either add short subsections (with WUMING_BACKEND value + required env vars) for the two missing backends, or keep the number in sync with what is actually documented. Verify the value against action.yml's backend description, and make sure the equivalent sentence in the Forgejo Actions guide is updated to the same number so the two guides do not contradict each other.

**docs** [MEDIUM] The backend count was bumped from four to six, but the "Choosing a backend" section that follows only documents four backends: DeepSeek (line 49), Kimi/Moonshot (ends line 102), OpenRouter (line 104) and the generic OpenAI-compatible endpoint (line 120). The max-tokens note further down (lines 160-162) also only names those same four as OpenAI-compatible, so readers are told there are six backends but can never discover the remaining two. Either add short subsections (with `WUMING_BACKEND` value + required env vars) for the two missing backends, or keep the number in sync with what is actually documented. Verify the value against `action.yml`'s `backend` description, and make sure the equivalent sentence in the Forgejo Actions guide is updated to the same number so the two guides do not contradict each other. <!-- wuming:sha256:aa6d9c3cbdc0a9a6c7040dbe05728a1dd17c3223c1133c075cfea3772a96ded3 -->
marvin8 marked this conversation as resolved
📝 clarify openrouter slug usage and local-endpoint guidance in the action
Some checks failed
/ pr-review (pull_request) Failing after 11s
/ gitleaks (pull_request) Successful in 31s
/ deploy-docs (pull_request) Has been cancelled
/ publish-container (pull_request) Has been cancelled
/ checks (pull_request) Has been cancelled
53e7d2e547
agent-pi force-pushed feat/issue-74-openrouter-own-review from 53e7d2e547
Some checks failed
/ pr-review (pull_request) Failing after 11s
/ gitleaks (pull_request) Successful in 31s
/ deploy-docs (pull_request) Has been cancelled
/ publish-container (pull_request) Has been cancelled
/ checks (pull_request) Has been cancelled
to 86df38571d
All checks were successful
/ gitleaks (pull_request) Successful in 34s
/ pr-review (pull_request) Successful in 1m47s
/ checks (pull_request) Successful in 1m49s
/ deploy-docs (pull_request) Has been skipped
/ publish-container (pull_request) Has been skipped
2026-10-15 10:20:44 +00:00
Compare
Author
Collaborator

action.yml

A02: The OpenAI-compatible base URL example includes an unencrypted HTTP endpoint (http://localhost:1234/v1)

✅ Fixed in commit 86df385 — the openai_base_url description now states: "Use HTTPS for remote endpoints; plain HTTP only for local servers." The localhost example stays because keyless local servers (LM Studio, vLLM) are the intended use case for plain HTTP, but the guidance is now explicit.

[`action.yml`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4940) > A02: The OpenAI-compatible base URL example includes an unencrypted HTTP endpoint (http://localhost:1234/v1) ✅ Fixed in commit `86df385` — the `openai_base_url` description now states: "Use HTTPS for remote endpoints; plain HTTP only for local servers." The localhost example stays because keyless local servers (LM Studio, vLLM) are the intended use case for plain HTTP, but the guidance is now explicit.
Author
Collaborator

action.yml

The agents input documentation was not updated for the new backends: it still shows only a bare model name

✅ Fixed in commit 86df385 — the agents description now shows a vendor/model example (security:moonshotai/kimi-k2.7-code), notes model names are backend-specific, and the openrouter_key description now says explicitly: "override per agent via agents: name:vendor/model (e.g. code:moonshotai/kimi-k2.7-code)".

[`action.yml`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4943) > The `agents` input documentation was not updated for the new backends: it still shows only a bare model name ✅ Fixed in commit `86df385` — the `agents` description now shows a `vendor/model` example (`security:moonshotai/kimi-k2.7-code`), notes model names are backend-specific, and the `openrouter_key` description now says explicitly: "override per agent via agents: name:vendor/model (e.g. code:moonshotai/kimi-k2.7-code)".
Author
Collaborator

docs/getting-started/deploying-on-woodpecker.md

The backend count was bumped from four to six, but the "Choosing a backend" section that follows only documents four backends

🔴 Not a defect — the section documents all six. Current headers in deploying-on-woodpecker.md: ### DeepSeek (default) (line 49), ### Anthropic (Claude) (line 64), ### Ollama (self-hosted) (line 77), ### Moonshot (Kimi) (line 90), ### OpenRouter (line 104), ### Any OpenAI-compatible endpoint (generic) (line 120). The diff hunks this review saw only contain the newly added sections, so the pre-existing Anthropic and Ollama sections were outside the reviewed context.

[`docs/getting-started/deploying-on-woodpecker.md`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4944) > The backend count was bumped from four to six, but the "Choosing a backend" section that follows only documents four backends 🔴 Not a defect — the section documents all six. Current headers in `deploying-on-woodpecker.md`: `### DeepSeek (default)` (line 49), `### Anthropic (Claude)` (line 64), `### Ollama (self-hosted)` (line 77), `### Moonshot (Kimi)` (line 90), `### OpenRouter` (line 104), `### Any OpenAI-compatible endpoint (generic)` (line 120). The diff hunks this review saw only contain the newly added sections, so the pre-existing Anthropic and Ollama sections were outside the reviewed context.
forgejo-actions left a comment

WuMing

Found 3 issue(s). See inline comments below.

## WuMing Found **3** issue(s). See inline comments below.
@ -52,2 +67,3 @@
default: ""
agents:
description: "Comma-separated list of agents, e.g. code:deepseek-v4-flash,security. Valid names: code, config, docs, performance, security, shell, tests."
description: "Comma-separated list of agents, e.g. code:deepseek-v4-flash,security:moonshotai/kimi-k2.7-code. Model names are backend-specific; OpenRouter uses vendor/model slugs. Valid names: code, config, docs, performance, security, shell, tests."

config [LOW]

The example model in the agents description (code:deepseek-v4-flash) is the old, un-prefixed slug and is not a valid OpenRouter vendor/model id, which directly contradicts the following sentence ('OpenRouter uses vendor/model slugs'). Users copying this example for backend=openrouter will get an invalid-model error. Update the example to a prefixed slug, e.g. code:deepseek/deepseek-v4.1-flash,security:moonshotai/kimi-k2.7-code, to match the values actually used in .forgejo/workflows/ci.yml.

**config** [LOW] The example model in the agents description (`code:deepseek-v4-flash`) is the old, un-prefixed slug and is not a valid OpenRouter vendor/model id, which directly contradicts the following sentence ('OpenRouter uses vendor/model slugs'). Users copying this example for backend=openrouter will get an invalid-model error. Update the example to a prefixed slug, e.g. `code:deepseek/deepseek-v4.1-flash,security:moonshotai/kimi-k2.7-code`, to match the values actually used in .forgejo/workflows/ci.yml. <!-- wuming:sha256:9c18e5bfd2c1cbd2bbb364cf501c2bce15df400b9d21363d52c4ef2a719e2bd3 -->
@ -96,2 +112,4 @@
WUMING_OLLAMA_URL: ${{ inputs.ollama_url }}
WUMING_MOONSHOT_KEY: ${{ inputs.moonshot_key }}
WUMING_OPENROUTER_KEY: ${{ inputs.openrouter_key }}
WUMING_OPENAI_BASE_URL: ${{ inputs.openai_base_url }}

security [HIGH]

The generic OpenAI backend passes a user-controlled openai_base_url directly to the HTTP client via WUMING_OPENAI_BASE_URL without allow-list validation. An attacker who can influence this input can redirect LLM requests to an internal or cloud metadata endpoint (e.g., 169.254.169.254) and exfiltrate the API key, constituting an A10 Server-Side Request Forgery vulnerability. Add an allow-list of permitted base URLs or restrict to known safe endpoints before use.

**security** [HIGH] The generic OpenAI backend passes a user-controlled `openai_base_url` directly to the HTTP client via `WUMING_OPENAI_BASE_URL` without allow-list validation. An attacker who can influence this input can redirect LLM requests to an internal or cloud metadata endpoint (e.g., 169.254.169.254) and exfiltrate the API key, constituting an A10 Server-Side Request Forgery vulnerability. Add an allow-list of permitted base URLs or restrict to known safe endpoints before use. <!-- wuming:sha256:5495a0f0fa1c1e7af229445e411a7442a6800d0173238755df9fd356fd6bf6e6 -->
@ -111,0 +115,4 @@
with:
forgejo_token: ${{ secrets.WUMING_FORGEJO_TOKEN }}
backend: openrouter
openrouter_key: ${{ secrets.WUMING_OPENROUTER_KEY }}

docs [LOW]

The example reads the secret WUMING_OPENROUTER_KEY, while the PR note says the secret that must exist on this repo is wuming_openrouter_key. Align the secret name/casing in the docs with the name actually used by ci.yml; otherwise the snippet is not copy-pasteable and readers will end up with an empty OpenRouter key.

**docs** [LOW] The example reads the secret `WUMING_OPENROUTER_KEY`, while the PR note says the secret that must exist on this repo is `wuming_openrouter_key`. Align the secret name/casing in the docs with the name actually used by `ci.yml`; otherwise the snippet is not copy-pasteable and readers will end up with an empty OpenRouter key. <!-- wuming:sha256:f18769519119d7d77d1a6d69683739dd58151a64607c9ec67936f36c4b8ed63b -->
🐛 fail fast on missing keys for hosted backends
All checks were successful
/ gitleaks (pull_request) Successful in 15s
/ checks (pull_request) Successful in 1m46s
/ deploy-docs (pull_request) Has been skipped
/ publish-container (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 2m9s
af7066a8fc
Author
Collaborator

.forgejo/workflows/ci.yml

The model assignments are not actually preserved: the four config/docs/shell/tests agents previously ran deepseek-v4-flash, but now use deepseek/deepseek-v4.1-flash

Correct observation, and the move is intentional (maintainer decision): V4.1 Flash is the current generation of the same DeepSeek flash line and matches the new openrouter preset default. The PR body's "preserving the model assignments" wording was inaccurate and has been corrected to state the version change explicitly. Exact parity was considered (deepseek/deepseek-v4-flash exists on OpenRouter) and declined in favour of the newer flash.

[`.forgejo/workflows/ci.yml`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4941) > The model assignments are not actually preserved: the four config/docs/shell/tests agents previously ran `deepseek-v4-flash`, but now use `deepseek/deepseek-v4.1-flash` Correct observation, and the move is intentional (maintainer decision): V4.1 Flash is the current generation of the same DeepSeek flash line and matches the new `openrouter` preset default. The PR body's "preserving the model assignments" wording was inaccurate and has been corrected to state the version change explicitly. Exact parity was considered (`deepseek/deepseek-v4-flash` exists on OpenRouter) and declined in favour of the newer flash.
Author
Collaborator

.forgejo/workflows/ci.yml

The job now depends on a repo secret wuming_openrouter_key that does not exist yet

✅ Two parts. The secret now exists — this PR's own pr-review ran end-to-end through OpenRouter and posted its review successfully. And the confusing-failure concern is addressed in commit af7066a: hosted backends (deepseek, moonshot, openrouter) now raise ValueError("WUMING_<KEY> is required when WUMING_BACKEND=<backend>") at startup when the key is empty. The generic openai backend deliberately still allows an empty key for keyless local servers, covered by a test.

[`.forgejo/workflows/ci.yml`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4942) > The job now depends on a repo secret `wuming_openrouter_key` that does not exist yet ✅ Two parts. The secret now exists — this PR's own `pr-review` ran end-to-end through OpenRouter and posted its review successfully. And the confusing-failure concern is addressed in commit `af7066a`: hosted backends (`deepseek`, `moonshot`, `openrouter`) now raise `ValueError("WUMING_<KEY> is required when WUMING_BACKEND=<backend>")` at startup when the key is empty. The generic `openai` backend deliberately still allows an empty key for keyless local servers, covered by a test.
forgejo-actions left a comment

WuMing

Found 6 issue(s). See inline comments below.

## WuMing Found **6** issue(s). See inline comments below.
@ -52,0 +58,4 @@
required: false
default: ""
openai_key:
description: API key for the generic OpenAI-compatible endpoint. Required when backend=openai.

config [LOW]

This description states the key is "Required when backend=openai", but the PR (and the fail-fast change it describes) explicitly keeps the generic openai backend keyless for local servers. This contradiction will mislead users of local OpenAI-compatible endpoints (e.g. http://localhost:1234/v1) into thinking a key is mandatory. Reword to reflect that the key is optional and only needed for endpoints that authenticate, e.g. "API key for the generic OpenAI-compatible endpoint. Optional; leave empty for keyless local servers."

**config** [LOW] This description states the key is "Required when backend=openai", but the PR (and the fail-fast change it describes) explicitly keeps the generic `openai` backend keyless for local servers. This contradiction will mislead users of local OpenAI-compatible endpoints (e.g. http://localhost:1234/v1) into thinking a key is mandatory. Reword to reflect that the key is optional and only needed for endpoints that authenticate, e.g. "API key for the generic OpenAI-compatible endpoint. Optional; leave empty for keyless local servers." <!-- wuming:sha256:55a01c0f3c18a5cfc6178a69062bcdba2df87ec54b637183bef7ac7a15c27027 -->
@ -259,3 +264,4 @@
)
return OpenAICompatibleBackend(
base_url,
getattr(config, key_attribute),

code [LOW]

The API key was already fetched into api_key on line 259 and validated. Calling getattr(config, key_attribute) again is redundant and slightly obscures the validated value. Pass api_key directly to OpenAICompatibleBackend.

**code** [LOW] The API key was already fetched into `api_key` on line 259 and validated. Calling `getattr(config, key_attribute)` again is redundant and slightly obscures the validated value. Pass `api_key` directly to `OpenAICompatibleBackend`. <!-- wuming:sha256:5b4582d58300df5dd2a21878a56913da3f2ef6c21a0b7466db8f98e3eeee6e40 -->
@ -814,6 +814,34 @@ def test_build_backend_openai_requires_base_url() -> None:
expect(raised).to_be_truthy()
@test("_build_backend raises ValueError when a hosted preset has an empty key")

code [LOW]

The looped test uses a single raised flag, so a failure does not indicate which backend failed to raise. Prefer parameterized tests or include backend_name in the assertion message for clearer diagnostics.

**code** [LOW] The looped test uses a single `raised` flag, so a failure does not indicate which backend failed to raise. Prefer parameterized tests or include `backend_name` in the assertion message for clearer diagnostics. <!-- wuming:sha256:cbe97a2b8ef2f0b86262868662f40b0d79eeae382be27a6e29351c041271079f -->
@ -817,0 +823,4 @@
]:
raised = False
try:
_build_backend(_make_config(backend_name), MagicMock())

code [MEDIUM]

The test relies on _make_config defaults to leave the relevant API key empty. If _make_config is later changed to provide a default key, this test will either stop exercising the empty-key path or fail with a confusing message. Explicitly clear the key for the backend under test, e.g. config = replace(_make_config(backend_name), **{key_attribute: ""}).

**code** [MEDIUM] The test relies on `_make_config` defaults to leave the relevant API key empty. If `_make_config` is later changed to provide a default key, this test will either stop exercising the empty-key path or fail with a confusing message. Explicitly clear the key for the backend under test, e.g. `config = replace(_make_config(backend_name), **{key_attribute: ""})`. <!-- wuming:sha256:88b14aa7500b97b9b7df485a7a8d05971b53e0174d346010793a7681943df5f1 -->
@ -817,0 +827,4 @@
except ValueError as error:
raised = True
env_var = f"WUMING_{key_attribute.upper()}"
expect(str(error)).to_contain(env_var)

code [LOW]

The assertion only checks that the env var name appears in the error message. It should also verify the backend name is included, e.g. expect(str(error)).to_contain(backend_name), to ensure the message matches the implementation.

**code** [LOW] The assertion only checks that the env var name appears in the error message. It should also verify the backend name is included, e.g. `expect(str(error)).to_contain(backend_name)`, to ensure the message matches the implementation. <!-- wuming:sha256:0c2cf7527cf8e38711c7a1f618cf7e4a11b8b9c8e8f89136d5ce4c57b37754dd -->
@ -817,0 +834,4 @@
@test("_build_backend allows an empty key for the generic openai backend")
def test_build_backend_openai_allows_empty_key() -> None:
config = replace(
_make_config("openai"),

code [MEDIUM]

The test claims to verify that the generic openai backend allows an empty key, but it does not explicitly set openai_key="". It depends on _make_config("openai") defaulting to an empty key. Add openai_key="" to the replace(...) call so the precondition is explicit and the test remains valid if defaults change.

**code** [MEDIUM] The test claims to verify that the generic openai backend allows an empty key, but it does not explicitly set `openai_key=""`. It depends on `_make_config("openai")` defaulting to an empty key. Add `openai_key=""` to the `replace(...)` call so the precondition is explicit and the test remains valid if defaults change. <!-- wuming:sha256:4dc3cf98450e46126fddaa08ed3dc1b40831bf41fc27cd9006365df306629d5b -->
Author
Collaborator

action.yml

The example model in the agents description (code:deepseek-v4-flash) is the old, un-prefixed slug and is not a valid OpenRouter vendor/model id

🔴 Not a defect — the description intentionally shows one example per style: code:deepseek-v4-flash is a valid model name for the DeepSeek backend (it is the DeepSeek preset's default model), and security:moonshotai/kimi-k2.7-code shows the OpenRouter vendor/model form. The sentence that follows — "Model names are backend-specific; OpenRouter uses vendor/model slugs" — explains exactly this distinction, so a reader on backend=openrouter is directed to the slug form.

[`action.yml`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4952) > The example model in the agents description (`code:deepseek-v4-flash`) is the old, un-prefixed slug and is not a valid OpenRouter vendor/model id 🔴 Not a defect — the description intentionally shows one example per style: `code:deepseek-v4-flash` is a valid model name for the DeepSeek backend (it is the DeepSeek preset's default model), and `security:moonshotai/kimi-k2.7-code` shows the OpenRouter `vendor/model` form. The sentence that follows — "Model names are backend-specific; OpenRouter uses vendor/model slugs" — explains exactly this distinction, so a reader on `backend=openrouter` is directed to the slug form.
Author
Collaborator

docs/getting-started/deploying-on-forgejo-actions.md

The example reads the secret WUMING_OPENROUTER_KEY, while the PR note says the secret that must exist on this repo is wuming_openrouter_key

🔴 Not a defect — the two names refer to different repos on purpose. The docs snippets are templates for readers configuring their own repos, where the secret name is whatever the reader creates (the guide's snippets consistently use the WUMING_* uppercase convention, as with WUMING_DEEPSEEK_KEY and WUMING_MOONSHOT_KEY). .forgejo/workflows/ci.yml references this repo's actual secret (wuming_openrouter_key), matching how this repo has always named its secrets (previously wuming_moonshot_key, wuming_deepseek_key).

[`docs/getting-started/deploying-on-forgejo-actions.md`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4953) > The example reads the secret `WUMING_OPENROUTER_KEY`, while the PR note says the secret that must exist on this repo is `wuming_openrouter_key` 🔴 Not a defect — the two names refer to different repos on purpose. The docs snippets are templates for readers configuring their own repos, where the secret name is whatever the reader creates (the guide's snippets consistently use the `WUMING_*` uppercase convention, as with `WUMING_DEEPSEEK_KEY` and `WUMING_MOONSHOT_KEY`). `.forgejo/workflows/ci.yml` references this repo's actual secret (`wuming_openrouter_key`), matching how this repo has always named its secrets (previously `wuming_moonshot_key`, `wuming_deepseek_key`).
🐛 pass validated key and align openai_key keyless wording
All checks were successful
/ gitleaks (pull_request) Successful in 33s
/ checks (pull_request) Successful in 1m50s
/ deploy-docs (pull_request) Has been skipped
/ publish-container (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 4m41s
6c6d8e9704
Author
Collaborator

src/wuming/main.py

✅ Fixed in commit 6c6d8e9 — the validated api_key local is now passed directly to OpenAICompatibleBackend.

[`src/wuming/main.py`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4958) ✅ Fixed in commit `6c6d8e9` — the validated `api_key` local is now passed directly to `OpenAICompatibleBackend`.
Author
Collaborator

tests/test_main.py

✅ Fixed in commit 6c6d8e9 — the parametrised test now explicitly clears the key: replace(_make_config(backend_name), **{key_attribute: ""}), so the empty-key precondition no longer depends on _make_config defaults.

[`tests/test_main.py`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4959) ✅ Fixed in commit `6c6d8e9` — the parametrised test now explicitly clears the key: `replace(_make_config(backend_name), **{key_attribute: ""})`, so the empty-key precondition no longer depends on `_make_config` defaults.
Author
Collaborator

tests/test_main.py

✅ Fixed in commit 6c6d8e9 — openai_key="" is now an explicit part of the replace(...) call.

[`tests/test_main.py`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4960) ✅ Fixed in commit `6c6d8e9` — `openai_key=""` is now an explicit part of the `replace(...)` call.
Author
Collaborator

tests/test_main.py

✅ Fixed in commit 6c6d8e9 — the test now asserts both WUMING_<KEY> and backend_name appear in the message.

[`tests/test_main.py`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4961) ✅ Fixed in commit `6c6d8e9` — the test now asserts both `WUMING_<KEY>` and `backend_name` appear in the message.
Author
Collaborator

tests/test_main.py

✅ Fixed in commit 6c6d8e9 — the loop is now a real parametrised test (@test(...).cases(...) with one case per backend), so each backend reports independently.

[`tests/test_main.py`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4962) ✅ Fixed in commit `6c6d8e9` — the loop is now a real parametrised test (`@test(...).cases(...)` with one case per backend), so each backend reports independently.
Author
Collaborator

action.yml

✅ Fixed in commit 6c6d8e9 — the openai_key description now reads: "May be empty for keyless local servers (e.g. LM Studio); required for hosted endpoints." The same contradiction was corrected in the README configuration table and the project agent docs (AGENTS.md), which carried the identical wording from #73.

[`action.yml`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4963) ✅ Fixed in commit `6c6d8e9` — the `openai_key` description now reads: "May be empty for keyless local servers (e.g. LM Studio); required for hosted endpoints." The same contradiction was corrected in the README configuration table and the project agent docs (AGENTS.md), which carried the identical wording from #73.
forgejo-actions left a comment

WuMing

Found 3 issue(s). See inline comments below.

## WuMing Found **3** issue(s). See inline comments below.
@ -257,2 +257,4 @@
if preset is not None:
base_url, default_model, key_attribute = preset
api_key = getattr(config, key_attribute)
if not api_key:

code [LOW]

The guard if not api_key rejects empty strings and None, but it accepts all-whitespace strings (e.g., " "). A whitespace-only key will be passed to the provider and fail later. Consider if not api_key or not api_key.strip(): and add a test for whitespace-only keys.

**code** [LOW] The guard `if not api_key` rejects empty strings and None, but it accepts all-whitespace strings (e.g., `" "`). A whitespace-only key will be passed to the provider and fail later. Consider `if not api_key or not api_key.strip():` and add a test for whitespace-only keys. <!-- wuming:sha256:1d978b2a34f7728c353ea08ac692fc025825b15d23709169bc7c55dc8e37f9c6 -->
@ -817,0 +828,4 @@
_build_backend(config, MagicMock())
except ValueError as error:
raised = True
expect(str(error)).to_contain(f"WUMING_{key_attribute.upper()}")

code [LOW]

Deriving the expected env-var name with the same key_attribute.upper() expression used in the implementation makes this assertion tautological. If the implementation used the wrong prefix or attribute name, the test would still pass. Hardcode the expected strings per backend (e.g., "WUMING_DEEPSEEK_KEY").

**code** [LOW] Deriving the expected env-var name with the same `key_attribute.upper()` expression used in the implementation makes this assertion tautological. If the implementation used the wrong prefix or attribute name, the test would still pass. Hardcode the expected strings per backend (e.g., `"WUMING_DEEPSEEK_KEY"`). <!-- wuming:sha256:f4b589c2cbd0ce37a90a318f1be8ce9032caaaddec581b5e33665f93acbf8273 -->
@ -817,0 +842,4 @@
openai_model="my-model",
)
backend = _build_backend(config, MagicMock())
expect(backend).not_.to_be_none()

code [LOW]

Only checking backend is not None is a weak assertion for the keyless-openai case. The test should also verify the returned object is an OpenAICompatibleBackend and that the empty key, base URL, and model were propagated correctly; otherwise regressions that silently substitute defaults could be missed.

**code** [LOW] Only checking `backend` is not None is a weak assertion for the keyless-openai case. The test should also verify the returned object is an `OpenAICompatibleBackend` and that the empty key, base URL, and model were propagated correctly; otherwise regressions that silently substitute defaults could be missed. <!-- wuming:sha256:cdad2e6621d6f748e57cdbadff925aed355aa1f7e2542bc9d622271d1e03ef64 -->
🐛 reject whitespace-only hosted keys and harden backend tests
All checks were successful
/ gitleaks (pull_request) Successful in 31s
/ checks (pull_request) Successful in 1m46s
/ deploy-docs (pull_request) Has been skipped
/ publish-container (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 4m24s
bd214a4d65
Author
Collaborator

src/wuming/main.py

✅ Fixed in commit bd214a4 — the guard is now if not api_key or not api_key.strip():, and a new test covers a whitespace-only deepseek_key raising the startup ValueError.

[`src/wuming/main.py`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4975) ✅ Fixed in commit `bd214a4` — the guard is now `if not api_key or not api_key.strip():`, and a new test covers a whitespace-only `deepseek_key` raising the startup `ValueError`.
Author
Collaborator

tests/test_main.py

✅ Fixed in commit bd214a4 — each case now carries a hardcoded expected_env_var (WUMING_DEEPSEEK_KEY, WUMING_MOONSHOT_KEY, WUMING_OPENROUTER_KEY) asserted verbatim, so a wrong prefix or attribute name in the implementation can no longer pass.

[`tests/test_main.py`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4976) ✅ Fixed in commit `bd214a4` — each case now carries a hardcoded `expected_env_var` (`WUMING_DEEPSEEK_KEY`, `WUMING_MOONSHOT_KEY`, `WUMING_OPENROUTER_KEY`) asserted verbatim, so a wrong prefix or attribute name in the implementation can no longer pass.
Author
Collaborator

tests/test_main.py

✅ Fixed in commit bd214a4 — the keyless-openai test is now behavioural: it routes a MockTransport through _build_backend, asserts the backend type, then verifies the propagated configuration end-to-end (request URL http://localhost:1234/v1/chat/completions, model my-model, and no Authorization header).

[`tests/test_main.py`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4977) ✅ Fixed in commit `bd214a4` — the keyless-openai test is now behavioural: it routes a MockTransport through `_build_backend`, asserts the backend type, then verifies the propagated configuration end-to-end (request URL `http://localhost:1234/v1/chat/completions`, model `my-model`, and no `Authorization` header).
forgejo-actions left a comment

WuMing

Found 13 issue(s). See inline comments below.

## WuMing Found **13** issue(s). See inline comments below.
@ -97,3 +97,3 @@
- `WUMING_OPENROUTER_KEY` — OpenRouter API key (required if backend=openrouter; default model `deepseek/deepseek-v4.1-flash`)
- `WUMING_OPENAI_BASE_URL` — base URL of any OpenAI-compatible endpoint (required if backend=openai)
- `WUMING_OPENAI_KEY` — API key for the generic OpenAI-compatible endpoint (required if backend=openai)
- `WUMING_OPENAI_KEY` — API key for the generic OpenAI-compatible endpoint; may be empty for keyless local servers (e.g. LM Studio)

docs [LOW]

Same wording problem as the README table: the previous '(required if backend=openai)' qualifier was removed entirely, while the WUMING_OPENAI_BASE_URL and WUMING_OPENAI_MODEL entries still say they are required. State that the key is required for hosted OpenAI-compatible endpoints and only optional for keyless local servers, to keep the env-var list internally consistent.

**docs** [LOW] Same wording problem as the README table: the previous '(required if backend=openai)' qualifier was removed entirely, while the `WUMING_OPENAI_BASE_URL` and `WUMING_OPENAI_MODEL` entries still say they are required. State that the key is required for hosted OpenAI-compatible endpoints and only optional for keyless local servers, to keep the env-var list internally consistent. <!-- wuming:sha256:35f46483da40a16093b7d5f0f2547c84ed3a6d0abcbdfd6984919d1593a63008 -->
@ -150,7 +150,7 @@ See the [Forgejo/GitHub Actions guide](https://wuming.marvin8.zone/getting-start
| `WUMING_MOONSHOT_KEY` | — | Moonshot (Kimi) API key (required if backend=moonshot) |
| `WUMING_OPENROUTER_KEY` | — | OpenRouter API key (required if backend=openrouter); default model `deepseek/deepseek-v4.1-flash`, override per agent via `name:model` using OpenRouter `vendor/model` slugs |
| `WUMING_OPENAI_BASE_URL` | — | Base URL of any OpenAI-compatible endpoint, e.g. `https://openrouter.ai/api/v1` (required if backend=openai) |

docs [LOW]

This row still advertises https://openrouter.ai/api/v1 as the example for the generic openai backend, which now duplicates the dedicated openrouter backend added in this PR and may steer users to the wrong (and less capable) configuration. Use an example from a provider without a dedicated backend (e.g. https://api.groq.com/openai/v1) and point readers to the openrouter backend for OpenRouter.

**docs** [LOW] This row still advertises `https://openrouter.ai/api/v1` as the example for the generic `openai` backend, which now duplicates the dedicated `openrouter` backend added in this PR and may steer users to the wrong (and less capable) configuration. Use an example from a provider without a dedicated backend (e.g. `https://api.groq.com/openai/v1`) and point readers to the `openrouter` backend for OpenRouter. <!-- wuming:sha256:4e453184593f62465569a680aae4686e571d1ea7db9e254e8d27ff34dea13501 -->
@ -151,3 +151,3 @@
| `WUMING_OPENROUTER_KEY` | — | OpenRouter API key (required if backend=openrouter); default model `deepseek/deepseek-v4.1-flash`, override per agent via `name:model` using OpenRouter `vendor/model` slugs |
| `WUMING_OPENAI_BASE_URL` | — | Base URL of any OpenAI-compatible endpoint, e.g. `https://openrouter.ai/api/v1` (required if backend=openai) |
| `WUMING_OPENAI_KEY` | — | API key for the generic OpenAI-compatible endpoint (required if backend=openai) |
| `WUMING_OPENAI_KEY` | — | API key for the generic OpenAI-compatible endpoint; may be empty for keyless local servers (e.g. LM Studio) |

docs [LOW]

The rewrite dropped the '(required if backend=openai)' qualifier that the two neighbouring rows (WUMING_OPENAI_BASE_URL, WUMING_OPENAI_MODEL) still carry, so the table no longer says when an API key is actually needed. Keep the requirement and add the keyless exception, e.g. 'API key for the generic OpenAI-compatible endpoint (required for hosted endpoints; may be empty for keyless local servers such as LM Studio)'.

**docs** [LOW] The rewrite dropped the '(required if backend=openai)' qualifier that the two neighbouring rows (`WUMING_OPENAI_BASE_URL`, `WUMING_OPENAI_MODEL`) still carry, so the table no longer says when an API key is actually needed. Keep the requirement and add the keyless exception, e.g. 'API key for the generic OpenAI-compatible endpoint (required for hosted endpoints; may be empty for keyless local servers such as LM Studio)'. <!-- wuming:sha256:fde41a6f083080eb0ac61677e2ebbd499436b79d3ac054e8ed3346922519b297 -->
@ -50,2 +50,4 @@
required: false
default: ""
openrouter_key:
description: "OpenRouter API key. Required when backend=openrouter. Default model deepseek/deepseek-v4.1-flash; override per agent via agents: name:vendor/model (e.g. code:moonshotai/kimi-k2.7-code)."

config [LOW]

The OpenRouter default model is documented inside the openrouter_key description instead of a dedicated input, and there is no way to change the default for all agents (only per-agent overrides via agents). This is inconsistent with the generic backend, which exposes openai_model as the default-model input. Consider adding an openrouter_model input (and WUMING_OPENROUTER_MODEL env mapping) or explicitly documenting that the OpenRouter default is fixed and must be overridden per agent.

**config** [LOW] The OpenRouter default model is documented inside the `openrouter_key` description instead of a dedicated input, and there is no way to change the default for all agents (only per-agent overrides via `agents`). This is inconsistent with the generic backend, which exposes `openai_model` as the default-model input. Consider adding an `openrouter_model` input (and `WUMING_OPENROUTER_MODEL` env mapping) or explicitly documenting that the OpenRouter default is fixed and must be overridden per agent. <!-- wuming:sha256:bd7ff49594c56f263f3f55b511d4421f71b9bcdeb8a988db02bca4b2c50b1baa -->
marvin8 marked this conversation as resolved
@ -52,0 +54,4 @@
required: false
default: ""
openai_base_url:
description: Base URL of any OpenAI-compatible endpoint (e.g. https://openrouter.ai/api/v1, http://localhost:1234/v1). Required when backend=openai. Use HTTPS for remote endpoints; plain HTTP only for local servers.

config [MEDIUM]

The generic openai backend accepts an arbitrary base URL, and nothing enforces the documented "Use HTTPS for remote endpoints" guidance. A misconfigured plain-HTTP remote URL will transmit openai_key in cleartext. Recommend rejecting non-HTTPS base URLs unless the host is a loopback/local address (e.g. localhost, 127.0.0.1, ::1), rather than relying on a doc comment.

**config** [MEDIUM] The generic `openai` backend accepts an arbitrary base URL, and nothing enforces the documented "Use HTTPS for remote endpoints" guidance. A misconfigured plain-HTTP remote URL will transmit `openai_key` in cleartext. Recommend rejecting non-HTTPS base URLs unless the host is a loopback/local address (e.g. localhost, 127.0.0.1, ::1), rather than relying on a doc comment. <!-- wuming:sha256:9ccbd21da469ec9c13a3affb5ce3f0bacbaaa6fb650f7340d1f1ac17e1509924 -->
marvin8 marked this conversation as resolved
@ -52,0 +62,4 @@
required: false
default: ""
openai_model:
description: Default model for the generic OpenAI-compatible endpoint. Required when backend=openai.

config [MEDIUM]

openai_model (and openai_base_url) are documented as required when backend=openai, but unlike the hosted backends they are not validated/failed fast. The PR explicitly adds fail-fast for deepseek/moonshot/openrouter keys while leaving openai unvalidated, so a user setting backend=openai without a model (or base URL) gets a late, opaque error from the HTTP client instead of a clear startup message. Add a startup check that errors out when backend=openai and openai_base_url/openai_model are empty.

**config** [MEDIUM] `openai_model` (and `openai_base_url`) are documented as required when backend=openai, but unlike the hosted backends they are not validated/failed fast. The PR explicitly adds fail-fast for deepseek/moonshot/openrouter keys while leaving `openai` unvalidated, so a user setting backend=openai without a model (or base URL) gets a late, opaque error from the HTTP client instead of a clear startup message. Add a startup check that errors out when backend=openai and openai_base_url/openai_model are empty. <!-- wuming:sha256:56abfea9b01c611446f8ef53f42d2045b653ddb8981be23f213d3c7ea527ae20 -->
@ -52,2 +67,3 @@
default: ""
agents:
description: "Comma-separated list of agents, e.g. code:deepseek-v4-flash,security. Valid names: code, config, docs, performance, security, shell, tests."
description: "Comma-separated list of agents, e.g. code:deepseek-v4-flash,security:moonshotai/kimi-k2.7-code. Model names are backend-specific; OpenRouter uses vendor/model slugs. Valid names: code, config, docs, performance, security, shell, tests."

config [LOW]

The example mixes model namespaces from two different backends in a single agents value (code:deepseek-v4-flash is a native DeepSeek model, security:moonshotai/kimi-k2.7-code is an OpenRouter slug). Since backend defaults to deepseek, a user copying this example verbatim would get an invalid model for the security agent (and the reverse with backend=openrouter). Give one example per backend, e.g. code:deepseek-v4-flash,security for deepseek and code:moonshotai/kimi-k2.7-code,security:deepseek/deepseek-v4.1-flash for openrouter.

**config** [LOW] The example mixes model namespaces from two different backends in a single `agents` value (`code:deepseek-v4-flash` is a native DeepSeek model, `security:moonshotai/kimi-k2.7-code` is an OpenRouter slug). Since `backend` defaults to `deepseek`, a user copying this example verbatim would get an invalid model for the `security` agent (and the reverse with backend=openrouter). Give one example per backend, e.g. `code:deepseek-v4-flash,security` for deepseek and `code:moonshotai/kimi-k2.7-code,security:deepseek/deepseek-v4.1-flash` for openrouter. <!-- wuming:sha256:28a0181373a8810129a3f9493da14138ed67957436bcb7f4a1c4819fdd455daf -->
@ -111,0 +121,4 @@
Obtain a key at [openrouter.ai/keys](https://openrouter.ai/keys). One account gives
access to models from many providers (DeepSeek, Moonshot, Anthropic, Google, Meta
and more). The default model is `deepseek/deepseek-v4.1-flash`; override per agent
with OpenRouter `vendor/model` slugs via per-agent model selection.

docs [MEDIUM]

The new OpenRouter section tells readers to override models 'with OpenRouter vendor/model slugs' but shows no example, and the only model-override example in this guide ('Selecting agents', code:deepseek-v4-pro,config,security) uses a bare, non-namespaced slug that is invalid on OpenRouter. Add a namespaced example here, e.g. agents: code:moonshotai/kimi-k2.7-code,config:deepseek/deepseek-v4.1-flash, and note that bare DeepSeek/Kimi slugs used elsewhere in this page are for the native backends only.

**docs** [MEDIUM] The new OpenRouter section tells readers to override models 'with OpenRouter `vendor/model` slugs' but shows no example, and the only model-override example in this guide ('Selecting agents', `code:deepseek-v4-pro,config,security`) uses a bare, non-namespaced slug that is invalid on OpenRouter. Add a namespaced example here, e.g. `agents: code:moonshotai/kimi-k2.7-code,config:deepseek/deepseek-v4.1-flash`, and note that bare DeepSeek/Kimi slugs used elsewhere in this page are for the native backends only. <!-- wuming:sha256:988acbeb6c29d607ca8ecbfbe5f0a74777d3e4fa75fed87934a7ea3837277768 -->
@ -111,0 +131,4 @@
forgejo_token: ${{ secrets.WUMING_FORGEJO_TOKEN }}
backend: openai
openai_base_url: https://llm.example/v1
openai_key: ${{ secrets.WUMING_OPENAI_KEY }}

docs [LOW]

The example always supplies openai_key from a secret, but the prose immediately below (lines 139-140) says the key may be empty for keyless local servers. Either drop the line from the example or add a comment such as '# omit for keyless local servers (e.g. LM Studio)' so the example and the note agree.

**docs** [LOW] The example always supplies `openai_key` from a secret, but the prose immediately below (lines 139-140) says the key may be empty for keyless local servers. Either drop the line from the example or add a comment such as '# omit for keyless local servers (e.g. LM Studio)' so the example and the note agree. <!-- wuming:sha256:9a568e9e74d6482f4bc6915a6b36bcd900ac2c5a58053e82e6433b44d2b2bfc1 -->
@ -104,0 +115,4 @@
Obtain a key at [openrouter.ai/keys](https://openrouter.ai/keys). One account gives
access to models from many providers (DeepSeek, Moonshot, Anthropic, Google, Meta
and more). The default model is `deepseek/deepseek-v4.1-flash`; override per agent
with OpenRouter `vendor/model` slugs via per-agent model selection.

docs [MEDIUM]

Same gap as the Forgejo guide: the OpenRouter section requires vendor/model slugs for per-agent overrides but gives no example, while the 'Selecting agents' example below uses code:deepseek-v4-pro, which would fail on OpenRouter. Add a namespaced example (e.g. WUMING_AGENTS: code:moonshotai/kimi-k2.7-code,config:deepseek/deepseek-v4.1-flash) so readers do not copy an invalid model slug.

**docs** [MEDIUM] Same gap as the Forgejo guide: the OpenRouter section requires `vendor/model` slugs for per-agent overrides but gives no example, while the 'Selecting agents' example below uses `code:deepseek-v4-pro`, which would fail on OpenRouter. Add a namespaced example (e.g. `WUMING_AGENTS: code:moonshotai/kimi-k2.7-code,config:deepseek/deepseek-v4.1-flash`) so readers do not copy an invalid model slug. <!-- wuming:sha256:25466cef6c43942ee2b5ba1474fe941545cc26422a485be208aec8a46040048e -->
@ -104,0 +123,4 @@
environment:
WUMING_BACKEND: openai
WUMING_OPENAI_BASE_URL: https://llm.example/v1
WUMING_OPENAI_KEY:

docs [LOW]

This example passes WUMING_OPENAI_KEY from a secret even though the note at lines 134-135 states it may be empty for keyless local servers such as LM Studio. Add a comment that this variable can be omitted for keyless endpoints, or provide a keyless variant of the snippet.

**docs** [LOW] This example passes `WUMING_OPENAI_KEY` from a secret even though the note at lines 134-135 states it may be empty for keyless local servers such as LM Studio. Add a comment that this variable can be omitted for keyless endpoints, or provide a keyless variant of the snippet. <!-- wuming:sha256:5f4174d9712ebfff31a10d451d2cc8e9f770d85316f9954e856173907a00c9fb -->
@ -257,2 +257,4 @@
if preset is not None:
base_url, default_model, key_attribute = preset
api_key = getattr(config, key_attribute)
if not api_key or not api_key.strip():

code [MEDIUM]

The key-validation guard applies indiscriminately to every entry in _OPENAI_COMPATIBLE_PRESETS. If the generic 'openai' backend is also represented in that dict, this check would reject keyless local servers and contradict the PR requirement that the generic openai backend still allows an empty key. Make the hosted-backend validation explicit—e.g. validate only for deepseek/moonshot/openrouter—or ensure openai is handled by a separate, keyless code path.

**code** [MEDIUM] The key-validation guard applies indiscriminately to every entry in _OPENAI_COMPATIBLE_PRESETS. If the generic 'openai' backend is also represented in that dict, this check would reject keyless local servers and contradict the PR requirement that the generic openai backend still allows an empty key. Make the hosted-backend validation explicit—e.g. validate only for deepseek/moonshot/openrouter—or ensure openai is handled by a separate, keyless code path. <!-- wuming:sha256:6fbbbb5f5086aed329157a04f0c651cdc509a3bc6a36af2ef3e0cecceef86f86 -->
marvin8 marked this conversation as resolved
@ -817,0 +835,4 @@
expected_env_var="WUMING_OPENROUTER_KEY",
),
)
def test_build_backend_hosted_preset_requires_key(

code [LOW]

The new tests only cover empty/whitespace keys for hosted presets. There is no positive test verifying that deepseek/moonshot/openrouter with a valid, non-empty key still successfully return an OpenAICompatibleBackend. Add a parameterized happy-path case to guard against regressions in the normal success path.

**code** [LOW] The new tests only cover empty/whitespace keys for hosted presets. There is no positive test verifying that deepseek/moonshot/openrouter with a valid, non-empty key still successfully return an OpenAICompatibleBackend. Add a parameterized happy-path case to guard against regressions in the normal success path. <!-- wuming:sha256:90c0c2c0a4be5ce175638f3030ea6002624272b9de75f5828185f42db135d77f -->
📝 add openrouter slug examples and clarify openai key wording
All checks were successful
/ gitleaks (pull_request) Successful in 32s
/ checks (pull_request) Successful in 1m49s
/ deploy-docs (pull_request) Has been skipped
/ publish-container (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 3m29s
673b9445c1
Author
Collaborator

README.md

✅ Fixed in commit 673b944 — the WUMING_OPENAI_KEY row now reads: "may be empty for keyless local servers (e.g. LM Studio), required for hosted endpoints", so the table states when the key is actually needed.

[`README.md`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4989) ✅ Fixed in commit `673b944` — the `WUMING_OPENAI_KEY` row now reads: "may be empty for keyless local servers (e.g. LM Studio), required for hosted endpoints", so the table states when the key is actually needed.
Author
Collaborator

AGENTS.md

✅ Fixed in commit 673b944 — same wording as the README row now: "may be empty for keyless local servers (e.g. LM Studio), required for hosted endpoints".

[`AGENTS.md`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4990) ✅ Fixed in commit `673b944` — same wording as the README row now: "may be empty for keyless local servers (e.g. LM Studio), required for hosted endpoints".
Author
Collaborator

README.md

✅ Fixed in commit 673b944 — the generic-backend row now uses a neutral example (https://llm.example/v1) instead of duplicating the OpenRouter URL, so readers are not steered onto the dedicated backend by accident.

[`README.md`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4995) ✅ Fixed in commit `673b944` — the generic-backend row now uses a neutral example (`https://llm.example/v1`) instead of duplicating the OpenRouter URL, so readers are not steered onto the dedicated backend by accident.
Author
Collaborator

docs/getting-started/deploying-on-forgejo-actions.md

✅ Fixed in commit 673b944 — the per-agent model section now includes an OpenRouter example: agents: code:moonshotai/kimi-k2.7-code,security:moonshotai/kimi-k2.7-code.

[`docs/getting-started/deploying-on-forgejo-actions.md`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4991) ✅ Fixed in commit `673b944` — the per-agent model section now includes an OpenRouter example: `agents: code:moonshotai/kimi-k2.7-code,security:moonshotai/kimi-k2.7-code`.
Author
Collaborator

docs/getting-started/deploying-on-woodpecker.md

✅ Fixed in commit 673b944 — same OpenRouter slug example added to the Woodpecker guide's per-agent section (WUMING_AGENTS: code:moonshotai/kimi-k2.7-code,...).

[`docs/getting-started/deploying-on-woodpecker.md`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4992) ✅ Fixed in commit `673b944` — same OpenRouter slug example added to the Woodpecker guide's per-agent section (`WUMING_AGENTS: code:moonshotai/kimi-k2.7-code,...`).
Author
Collaborator

docs/getting-started/deploying-on-forgejo-actions.md

✅ Fixed in commit 673b944 — the openai_key line in the example now carries the comment # omit for keyless local servers, matching the prose.

[`docs/getting-started/deploying-on-forgejo-actions.md`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4993) ✅ Fixed in commit `673b944` — the `openai_key` line in the example now carries the comment `# omit for keyless local servers`, matching the prose.
Author
Collaborator

docs/getting-started/deploying-on-woodpecker.md

✅ Fixed in commit 673b944 — same comment added to the Woodpecker example (# omit for keyless local servers).

[`docs/getting-started/deploying-on-woodpecker.md`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4994) ✅ Fixed in commit `673b944` — same comment added to the Woodpecker example (`# omit for keyless local servers`).
Author
Collaborator

src/wuming/main.py

🔴 Not reachable — _OPENAI_COMPATIBLE_PRESETS contains only the three hosted presets (deepseek, moonshot, openrouter); the generic openai backend is handled by a separate branch in _build_backend with its own checks and deliberately no key requirement. A future entry in that dict would by definition be a hosted preset with a key requirement, so the guard's placement matches the data it guards.

[`src/wuming/main.py`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4983) 🔴 Not reachable — `_OPENAI_COMPATIBLE_PRESETS` contains only the three hosted presets (`deepseek`, `moonshot`, `openrouter`); the generic `openai` backend is handled by a separate branch in `_build_backend` with its own checks and deliberately no key requirement. A future entry in that dict would by definition be a hosted preset with a key requirement, so the guard's placement matches the data it guards.
Author
Collaborator

tests/test_main.py

🔴 False premise — positive coverage already exists: test_build_backend_deepseek_preset, test_build_backend_moonshot_preset, and test_build_backend_openrouter_preset each set a non-empty key, build the backend, and complete a full request through a MockTransport, asserting endpoint URL, default model, and auth header. These predate the key-validation change.

[`tests/test_main.py`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4984) 🔴 False premise — positive coverage already exists: `test_build_backend_deepseek_preset`, `test_build_backend_moonshot_preset`, and `test_build_backend_openrouter_preset` each set a non-empty key, build the backend, and complete a full request through a MockTransport, asserting endpoint URL, default model, and auth header. These predate the key-validation change.
Author
Collaborator

action.yml

🔴 False premise — WUMING_OPENAI_BASE_URL and WUMING_OPENAI_MODEL are fail-fast validated in _build_backend (ValueError at startup when either is empty), since commit d98e9d0. Covered by test_build_backend_openai_requires_base_url and test_build_backend_openai_requires_model.

[`action.yml`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4985) 🔴 False premise — `WUMING_OPENAI_BASE_URL` and `WUMING_OPENAI_MODEL` are fail-fast validated in `_build_backend` (`ValueError` at startup when either is empty), since commit `d98e9d0`. Covered by `test_build_backend_openai_requires_base_url` and `test_build_backend_openai_requires_model`.
Author
Collaborator

action.yml

🔴 Same rationale as the earlier reply on the agents description (see the 🔴 reply to the previous wave's finding): the two examples deliberately illustrate both naming styles — a bare slug for the DeepSeek backend and a vendor/model slug for OpenRouter — directly matching the "Model names are backend-specific" sentence that follows. The description is backend-agnostic: it cannot use only one namespace without being wrong for the other backend.

[`action.yml`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4988) 🔴 Same rationale as the earlier reply on the `agents` description (see the 🔴 reply to the previous wave's finding): the two examples deliberately illustrate both naming styles — a bare slug for the DeepSeek backend and a `vendor/model` slug for OpenRouter — directly matching the "Model names are backend-specific" sentence that follows. The description is backend-agnostic: it cannot use only one namespace without being wrong for the other backend.
Author
Collaborator

src/wuming/main.py

🔴 Declined (maintainer decision). WUMING_OPENAI_BASE_URL is operator-controlled CI configuration: whoever can set it in a workflow already has write access to the repo and its secrets, so there is no attacker-controlled input path — the threat model for SSRF exfiltration doesn't apply. Allow-listing endpoints would defeat the purpose of the generic backend (any OpenAI-compatible endpoint), and the hosted presets (deepseek, moonshot, openrouter) use fixed URLs and are unaffected.

[`src/wuming/main.py`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4951) 🔴 Declined (maintainer decision). `WUMING_OPENAI_BASE_URL` is operator-controlled CI configuration: whoever can set it in a workflow already has write access to the repo and its secrets, so there is no attacker-controlled input path — the threat model for SSRF exfiltration doesn't apply. Allow-listing endpoints would defeat the purpose of the generic backend (any OpenAI-compatible endpoint), and the hosted presets (`deepseek`, `moonshot`, `openrouter`) use fixed URLs and are unaffected.
Author
Collaborator

action.yml

🔴 Declined (maintainer decision, same family as the SSRF finding on src/wuming/main.py). The "Use HTTPS for remote endpoints" wording is guidance for operators, not an enforced invariant: the generic backend explicitly supports plain-HTTP local servers (LM Studio, vLLM), and the operator choosing a remote HTTP URL is the same trust level that already controls the workflow and its secrets. Enforcement here would block legitimate setups without closing a real attack path.

[`action.yml`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4986) 🔴 Declined (maintainer decision, same family as the SSRF finding on `src/wuming/main.py`). The "Use HTTPS for remote endpoints" wording is guidance for operators, not an enforced invariant: the generic backend explicitly supports plain-HTTP local servers (LM Studio, vLLM), and the operator choosing a remote HTTP URL is the same trust level that already controls the workflow and its secrets. Enforcement here would block legitimate setups without closing a real attack path.
Author
Collaborator

action.yml

🔁 Tracked as a follow-up: marvin8/wuming#76 — "Allow a global default-model override for the openrouter backend". The per-agent name:vendor/model syntax covers per-run choices today; a global default knob for hosted presets (parallel to WUMING_OPENAI_MODEL on the generic backend) is a feature, not a defect in this PR.

[`action.yml`](https://forge.marvin8.zone/marvin8/wuming/pulls/75#issuecomment-4987) 🔁 Tracked as a follow-up: marvin8/wuming#76 — "Allow a global default-model override for the openrouter backend". The per-agent `name:vendor/model` syntax covers per-run choices today; a global default knob for hosted presets (parallel to `WUMING_OPENAI_MODEL` on the generic backend) is a feature, not a defect in this PR.
marvin8 approved these changes 2026-10-15 20:13:59 +00:00
marvin8 manually merged commit 85150276d6 into main 2026-10-15 20:15:11 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
marvin8/wuming!75
No description provided.