Skip to content

feat: add generic CLI adapter and migrate Kimi#140

Merged
jacsamell merged 9 commits into
mainfrom
feat/generic-cli-adapter
Apr 29, 2026
Merged

feat: add generic CLI adapter and migrate Kimi#140
jacsamell merged 9 commits into
mainfrom
feat/generic-cli-adapter

Conversation

@jacsamell
Copy link
Copy Markdown
Contributor

@jacsamell jacsamell commented Apr 28, 2026

Summary

  • Add a config-backed GenericCLIAdapter for command-template CLI integrations.
  • Migrate kimi_k26_openrouter to generic-cli:kimi while keeping KimiParser explicit.
  • Support command rendering, resume args, env files, install checks, startup/prompt synthetic events, and parser selection for generic-cli:<name>.
  • Remove the concrete Kimi adapter to avoid duplicate/throwaway adapter code.

Test plan

  • python -m pytest tests/cli/test_adapters.py tests/core/test_user_config.py -q
  • python -m pytest tests/ -q
  • Live smoke: get_adapter("generic-cli:kimi") with kimi_k26_openrouter

This PR introduces a config-driven GenericCLIAdapter and migrates the existing Kimi integration to use it (generic-cli:kimi), consolidating CLI integrations and removing duplicate adapter code. It also adds a qwen mapping and a new QwenParser.

Key changes

  • GenericCLIAdapter

    • New config-backed CLI adapter with template-driven command rendering ({{model}}, {{prompt}}, {{worktree}}, {{session_id}}, {{env:VAR}})
    • Loads key/value pairs from configured env files into the subprocess environment without overwriting existing keys
    • install_hint and installed check support; supports resume args/templates and resume semantics
    • Emits synthetic startup/prompt events, streams subprocess output, writes lines to master log when available, and enforces configurable output timeout (default 600s)
    • Exposes async run signature: run(worktree, model, prompt, session_id=None, resume=False)
  • Configuration

    • Adds CubeConfig.generic_cli and example entries in python/cube.yaml for generic_cli.kimi and generic_cli.qwen
    • Routes cli_tools.kimi_k26_openrouter → "generic-cli:kimi" and adds cli_tools.qwen/qwen3.6-plus → "generic-cli:qwen"
    • Adds model_aliases.qwen → "qwen/qwen3.6-plus" and updates judges.judge_3 to use qwen (label updated)
  • Registry & parsers

    • Adapter registry recognizes "generic-cli:" and constructs GenericCLIAdapter from generic_cli config (with clear error when config missing)
    • Parser registry supports "generic-cli:" by reading an optional parser field from generic_cli config
    • Adds QwenParser (stream-json parser mapping system/assistant/user/result messages, tool/result handling, and supports_resume=True) and exposes it at package level
  • Removals & exports

    • Removes the concrete KimiAdapter implementation (python/cube/core/adapters/kimi.py)
    • Exposes GenericCLIAdapter from adapters package (replaces KimiAdapter export)
  • Tests

    • Tests updated to use GenericCLIAdapter for Kimi scenarios and retargeted mocks to generic_cli
    • New tests for Qwen parser, env-file resolution (OPENROUTER_API_KEY), parser registry, resume behaviour and master-log writes (tests/cli/test_adapters.py, tests/core/test_user_config.py)

Why it matters

  • Centralises CLI integration logic in a reusable, configurable adapter to reduce duplication and simplify adding new CLI-backed models (e.g. qwen) while preserving parser selection, resume semantics and env-file handling.

Testing

  • Unit: python -m pytest tests/cli/test_adapters.py tests/core/test_user_config.py -q (or run full tests)
  • Live smoke: get_adapter("generic-cli:kimi") with kimi_k26_openrouter

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Replaces the dedicated Kimi adapter with a config-driven GenericCLIAdapter, adds generic_cli entries for kimi and qwen in python/cube.yaml, introduces QwenParser, updates registry/parser resolution for generic-cli:<name>, and adapts tests to the new generic CLI flow.

Changes

Cohort / File(s) Summary
Configuration
python/cube.yaml
Adds generic_cli blocks for kimi and qwen; introduces model_aliases.qwen; maps qwen/qwen3.6-plusgeneric-cli:qwen; re-routes cli_tools.kimi_k26_openroutergeneric-cli:kimi; updates judges.judge_3 to use qwen and renames label.
New generic adapter
python/cube/core/adapters/generic_cli.py, python/cube/core/adapters/__init__.py
Adds GenericCLIAdapter (template-rendered command and resume parts, env-file loading without overwriting, synthetic events, streaming subprocess integration, master-log writes); exports updated to expose GenericCLIAdapter.
Kimi adapter removal
python/cube/core/adapters/kimi.py
Removes the previous KimiAdapter implementation and its helpers.
Registry & parser wiring
python/cube/core/adapters/registry.py, python/cube/core/parsers/registry.py, python/cube/core/user_config.py
Registry gains generic-cli:<name> parsing and a _get_generic_adapter helper; parser resolution supports generic-cli:<name> indirection and adds qwen mapping; CubeConfig now holds a generic_cli map loaded from YAML.
Parsers
python/cube/core/parsers/qwen.py, python/cube/core/parsers/__init__.py
Adds QwenParser (stream-json → StreamMessage conversion, tool/thinking/result handling, error handling, supports resume) and exports it at package level.
Tests
tests/cli/test_adapters.py, tests/core/test_user_config.py
Refactors tests to use GenericCLIAdapter, adds Qwen-specific tests (env-file variable interpolation and parser behaviour), updates registry/parser tests, and verifies generic_cli config loads.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • feat: opencode-adapter #135: Adds the original Kimi adapter/parser and registry entries that this PR migrates to the generic-cli approach.

Poem

🐰 I nibbled YAML, chewed a little string,
Built a generic runner so CLIs can sing.
I tuck env carrots in a tidy row,
Resume when you call — hop, off we go!
Logs hum softly, streaming lights aglow.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title 'feat: add generic CLI adapter and migrate Kimi' accurately reflects the main changes: introducing a new GenericCLIAdapter and migrating the Kimi integration to use it.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@python/cube/core/adapters/generic_cli.py`:
- Around line 38-43: Before calling run_subprocess_streaming, validate the
rendered command list built in cmd (from self.config.get("command", [])) and the
resume extension (from self.config.get("resume", [])): ensure the base "command"
config exists and renders to a non-empty list of non-empty strings; if resume
and session_id are used, validate resume renders similarly. If validation fails,
raise a ValueError with a clear message indicating which config ("command" or
"resume") is missing or malformed. Use the existing self._render to produce the
rendered parts for validation and keep the failure fast before invoking
run_subprocess_streaming.

In `@python/cube/core/parsers/registry.py`:
- Around line 23-30: When resolving "generic-cli:" mappings (the
cli_name.startswith("generic-cli:") branch), detect and fail-fast on unknown or
cyclic parser references: read generic_name from load_config().generic_cli,
obtain parser_name, and if parser_name is missing or get_parser(parser_name)
would return the default CursorParser unexpectedly, raise a clear
ConfigurationError; additionally implement a simple cycle-guard when recursing
through get_parser (e.g., pass a visited set of parser names and raise
ConfigurationError on repeats) so cyclic mappings are detected instead of
silently falling back to CursorParser.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 37105486-4afb-4151-bf09-813fbbdaa4ee

📥 Commits

Reviewing files that changed from the base of the PR and between 26ef522 and 7ad599f.

📒 Files selected for processing (9)
  • python/cube.yaml
  • python/cube/core/adapters/__init__.py
  • python/cube/core/adapters/generic_cli.py
  • python/cube/core/adapters/kimi.py
  • python/cube/core/adapters/registry.py
  • python/cube/core/parsers/registry.py
  • python/cube/core/user_config.py
  • tests/cli/test_adapters.py
  • tests/core/test_user_config.py
💤 Files with no reviewable changes (1)
  • python/cube/core/adapters/kimi.py
📜 Review details
🔇 Additional comments (7)
python/cube/core/user_config.py (1)

59-59: Solid config plumbing for generic_cli.

The new field is consistently loaded and propagated, with safe {} fallback defaults.

Also applies to: 191-192, 231-231

tests/core/test_user_config.py (1)

165-165: Nice coverage update for generic_cli loading.

This validates the new config path end-to-end in load_config().

Also applies to: 184-184

python/cube/core/adapters/__init__.py (1)

8-8: Export surface is correctly updated.

GenericCLIAdapter is now properly exposed for import consumers.

Also applies to: 16-16

python/cube.yaml (1)

35-35: Config migration to generic-cli:kimi looks coherent.

The new generic_cli.kimi block matches the adapter’s expected fields and keeps parser mapping explicit.

Also applies to: 39-63

python/cube/core/adapters/registry.py (1)

27-35: Generic adapter routing is cleanly integrated.

The explicit generic-cli:<name> path plus config-backed factory keeps the registry extensible without per-tool adapter classes.

Also applies to: 52-58

tests/cli/test_adapters.py (1)

80-99: Good migration of adapter tests to generic CLI behaviour.

Coverage still exercises command construction, resume handling, and registry/parser resolution after the refactor.

Also applies to: 147-154, 279-281

python/cube/core/adapters/generic_cli.py (1)

58-79: Env-file merge behaviour is well handled.

Loading configured .env files without overriding already-exported environment variables is a sensible default for local and CI runs.

Comment on lines +38 to +43
cmd = [self._render(part, context) for part in self.config.get("command", [])]
if resume and session_id:
cmd.extend(self._render(part, context) for part in self.config.get("resume", []))

timeout = float(self.config.get("output_timeout_seconds", 600))
async for line in run_subprocess_streaming(cmd, worktree, self.name, env=env, output_timeout=timeout):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate command config before subprocess execution.

At Line 38, an empty or malformed command config reaches subprocess launch and fails with a low-level error. Raise a clear ValueError early for actionable misconfiguration.

Suggested fix
-        cmd = [self._render(part, context) for part in self.config.get("command", [])]
+        command_template = self.config.get("command")
+        if not isinstance(command_template, list) or not command_template:
+            raise ValueError(f"generic_cli.{self.name}.command must be a non-empty list[str]")
+        if not all(isinstance(part, str) for part in command_template):
+            raise ValueError(f"generic_cli.{self.name}.command entries must be strings")
+
+        cmd = [self._render(part, context) for part in command_template]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cube/core/adapters/generic_cli.py` around lines 38 - 43, Before
calling run_subprocess_streaming, validate the rendered command list built in
cmd (from self.config.get("command", [])) and the resume extension (from
self.config.get("resume", [])): ensure the base "command" config exists and
renders to a non-empty list of non-empty strings; if resume and session_id are
used, validate resume renders similarly. If validation fails, raise a ValueError
with a clear message indicating which config ("command" or "resume") is missing
or malformed. Use the existing self._render to produce the rendered parts for
validation and keep the failure fast before invoking run_subprocess_streaming.

Comment on lines +23 to +30
if cli_name.startswith("generic-cli:"):
from ..user_config import load_config

generic_name = cli_name.split(":", 1)[1]
generic_config = load_config().generic_cli.get(generic_name, {})
parser_name = generic_config.get("parser")
if parser_name:
return get_parser(parser_name)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail fast on invalid or cyclic parser mappings.

At Line 29, recursive parser resolution has no cycle guard, and unknown configured parsers can silently fall back to CursorParser. That makes config mistakes hard to detect and can parse output incorrectly.

Suggested fix
-def get_parser(cli_name: str) -> ParserAdapter:
+def get_parser(cli_name: str, _seen: set[str] | None = None) -> ParserAdapter:
     """Get parser for CLI tool."""
+    seen = _seen or set()
+    if cli_name in seen:
+        raise ValueError(f"Circular parser mapping detected: {cli_name}")
+    seen.add(cli_name)
+
     if cli_name.startswith("generic-cli:"):
         from ..user_config import load_config

         generic_name = cli_name.split(":", 1)[1]
         generic_config = load_config().generic_cli.get(generic_name, {})
         parser_name = generic_config.get("parser")
         if parser_name:
-            return get_parser(parser_name)
+            if not parser_name.startswith("generic-cli:") and parser_name not in _PARSERS:
+                raise ValueError(f"Unknown parser '{parser_name}' for '{cli_name}'")
+            return get_parser(parser_name, seen)

     parser_class = _PARSERS.get(cli_name, CursorParser)
     return parser_class()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if cli_name.startswith("generic-cli:"):
from ..user_config import load_config
generic_name = cli_name.split(":", 1)[1]
generic_config = load_config().generic_cli.get(generic_name, {})
parser_name = generic_config.get("parser")
if parser_name:
return get_parser(parser_name)
def get_parser(cli_name: str, _seen: set[str] | None = None) -> ParserAdapter:
"""Get parser for CLI tool."""
seen = _seen or set()
if cli_name in seen:
raise ValueError(f"Circular parser mapping detected: {cli_name}")
seen.add(cli_name)
if cli_name.startswith("generic-cli:"):
from ..user_config import load_config
generic_name = cli_name.split(":", 1)[1]
generic_config = load_config().generic_cli.get(generic_name, {})
parser_name = generic_config.get("parser")
if parser_name:
if not parser_name.startswith("generic-cli:") and parser_name not in _PARSERS:
raise ValueError(f"Unknown parser '{parser_name}' for '{cli_name}'")
return get_parser(parser_name, seen)
parser_class = _PARSERS.get(cli_name, CursorParser)
return parser_class()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cube/core/parsers/registry.py` around lines 23 - 30, When resolving
"generic-cli:" mappings (the cli_name.startswith("generic-cli:") branch), detect
and fail-fast on unknown or cyclic parser references: read generic_name from
load_config().generic_cli, obtain parser_name, and if parser_name is missing or
get_parser(parser_name) would return the default CursorParser unexpectedly,
raise a clear ConfigurationError; additionally implement a simple cycle-guard
when recursing through get_parser (e.g., pass a visited set of parser names and
raise ConfigurationError on repeats) so cyclic mappings are detected instead of
silently falling back to CursorParser.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
python/cube/core/parsers/qwen.py (1)

12-14: Consider moving import json to module level.

Importing inside the method adds minor overhead on each call. Moving to module level is idiomatic.

Proposed change
 """Qwen Code CLI stream-json parser."""

+import json
 from typing import Any, Optional

 from ...models.types import StreamMessage
 from .base import ParserAdapter

Then remove line 14.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cube/core/parsers/qwen.py` around lines 12 - 14, The local import of
json inside the parse method should be moved to the module level to avoid
per-call overhead and follow Python conventions: add "import json" at the top of
the module and remove the "import json" line from the parse(self, line: str) ->
Optional[StreamMessage] method; ensure the parse function (and any other
functions in this file) continue to reference json normally after the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@python/cube/core/adapters/generic_cli.py`:
- Line 44: Wrap the conversion to
float(self.config.get("output_timeout_seconds", 600)) in a try/except that
catches ValueError and TypeError, capture the raw value from
self.config.get("output_timeout_seconds"), log an error including the key name
and invalid value (using the component logger available in this class), and fall
back to the default timeout (600) by assigning timeout = 600; keep the variable
name timeout and the config key "output_timeout_seconds" so callers of this
method/class (e.g., methods in class GenericCLI or its method that contains this
line) continue to work unchanged.

In `@python/cube/core/parsers/qwen.py`:
- Around line 73-77: The join fails if any thinking_parts element is None;
update the comprehension or build thinking_parts in the qwen parser so it only
collects non-None string values (e.g., filter out parts where
part.get("thinking") is None or coerce to str) before calling "\n".join; locate
the thinking_parts creation and the StreamMessage return in
python/cube/core/parsers/qwen.py and ensure the value passed to
StreamMessage(content=...) is a joined string of only valid strings (no None).

---

Nitpick comments:
In `@python/cube/core/parsers/qwen.py`:
- Around line 12-14: The local import of json inside the parse method should be
moved to the module level to avoid per-call overhead and follow Python
conventions: add "import json" at the top of the module and remove the "import
json" line from the parse(self, line: str) -> Optional[StreamMessage] method;
ensure the parse function (and any other functions in this file) continue to
reference json normally after the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d4593496-1ba8-4ba4-bc11-cff096e86b5d

📥 Commits

Reviewing files that changed from the base of the PR and between 7ad599f and d6970bb.

📒 Files selected for processing (6)
  • python/cube.yaml
  • python/cube/core/adapters/generic_cli.py
  • python/cube/core/parsers/__init__.py
  • python/cube/core/parsers/qwen.py
  • python/cube/core/parsers/registry.py
  • tests/cli/test_adapters.py
✅ Files skipped from review due to trivial changes (1)
  • python/cube/core/parsers/init.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • python/cube.yaml
📜 Review details
🔇 Additional comments (10)
python/cube/core/adapters/generic_cli.py (3)

40-42: Validate command config before subprocess execution.

An empty or malformed command config will result in launching an empty subprocess command, failing with a low-level error. Validate early to provide actionable error messages.


60-81: LGTM – env file loading is well-designed.

The implementation correctly:

  • Avoids overwriting existing shell env vars (line 78 check)
  • Handles missing files gracefully (line 67-68)
  • Strips quotes from values (line 77)
  • Skips comments and malformed lines (line 72)

83-90: LGTM – template rendering is clean.

The _render method handles both context placeholders and {{env:VAR}} lookups appropriately, with safe fallback to empty string for missing env vars.

python/cube/core/parsers/registry.py (2)

25-32: Add cycle guard and validate parser mappings.

Recursive parser resolution at line 32 has no cycle detection; unknown configured parsers silently fall back to CursorParser, making configuration errors hard to diagnose.


11-18: LGTM – parser registry updates.

The addition of QwenParser to the registry and import is correct.

tests/cli/test_adapters.py (3)

79-143: LGTM – comprehensive adapter test.

Good coverage of:

  • Command template rendering with {{model}}, {{worktree}}, {{prompt}}
  • Synthetic event emission (system init + user content)
  • Master log write calls
  • Output streaming

285-329: LGTM – env variable resolution test.

Excellent test for {{env:OPENROUTER_API_KEY}} resolution from worktree .env file, with proper isolation using patch.dict("os.environ", {}, clear=True).


332-417: LGTM – thorough parser coverage.

The test exercises all message types: system, thinking, tool_use, tool_result, text, and result. Validates both type mapping and field extraction.

python/cube/core/parsers/qwen.py (2)

85-88: LGTM – text parts handling is correct.

Unlike thinking_parts, this correctly filters with isinstance(part, str) and part before joining.


130-141: LGTM – tool name normalisation is clean.

The mapping covers common Qwen tool names with a sensible fallback for unknown tools.

if resume and session_id:
cmd.extend(self._render(part, context, env) for part in self.config.get("resume", []))

timeout = float(self.config.get("output_timeout_seconds", 600))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle non-numeric timeout config gracefully.

If output_timeout_seconds contains an invalid value (e.g., a non-numeric string), float() raises a ValueError with no context about which config key is at fault.

Proposed fix
-        timeout = float(self.config.get("output_timeout_seconds", 600))
+        raw_timeout = self.config.get("output_timeout_seconds", 600)
+        try:
+            timeout = float(raw_timeout)
+        except (TypeError, ValueError):
+            raise ValueError(f"generic_cli.{self.name}.output_timeout_seconds must be numeric, got: {raw_timeout!r}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
timeout = float(self.config.get("output_timeout_seconds", 600))
raw_timeout = self.config.get("output_timeout_seconds", 600)
try:
timeout = float(raw_timeout)
except (TypeError, ValueError):
raise ValueError(f"generic_cli.{self.name}.output_timeout_seconds must be numeric, got: {raw_timeout!r}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cube/core/adapters/generic_cli.py` at line 44, Wrap the conversion to
float(self.config.get("output_timeout_seconds", 600)) in a try/except that
catches ValueError and TypeError, capture the raw value from
self.config.get("output_timeout_seconds"), log an error including the key name
and invalid value (using the component logger available in this class), and fall
back to the default timeout (600) by assigning timeout = 600; keep the variable
name timeout and the config key "output_timeout_seconds" so callers of this
method/class (e.g., methods in class GenericCLI or its method that contains this
line) continue to work unchanged.

Comment on lines +73 to +77
thinking_parts = [
part.get("thinking") for part in content if isinstance(part, dict) and part.get("type") == "thinking"
]
if thinking_parts:
return StreamMessage(type="thinking", session_id=session_id, model=model, content="\n".join(thinking_parts))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

"\n".join(thinking_parts) will fail if any element is None.

The list comprehension collects part.get("thinking") which may return None. Joining None values raises TypeError: sequence item X: expected str instance, NoneType found.

Proposed fix
     thinking_parts = [
-        part.get("thinking") for part in content if isinstance(part, dict) and part.get("type") == "thinking"
+        part.get("thinking") for part in content if isinstance(part, dict) and part.get("type") == "thinking" and part.get("thinking")
     ]
     if thinking_parts:
         return StreamMessage(type="thinking", session_id=session_id, model=model, content="\n".join(thinking_parts))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cube/core/parsers/qwen.py` around lines 73 - 77, The join fails if any
thinking_parts element is None; update the comprehension or build thinking_parts
in the qwen parser so it only collects non-None string values (e.g., filter out
parts where part.get("thinking") is None or coerce to str) before calling
"\n".join; locate the thinking_parts creation and the StreamMessage return in
python/cube/core/parsers/qwen.py and ensure the value passed to
StreamMessage(content=...) is a joined string of only valid strings (no None).

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@python/cube.yaml`:
- Line 46: Remove the unused supports_resume YAML key: delete the
supports_resume: true entry (and the duplicate at line 70) from python/cube.yaml
because resume is controlled by the resume parameter passed to adapter.run() and
parser classes expose supports_resume() via code; ensure no code references this
YAML key remain (check for any lookups of "supports_resume" and remove them) and
keep GenericCLIAdapter behavior unchanged (resume logic stays in adapter.run()).
- Around line 78-79: Remove the command-line API key argument entries
"--openai-api-key" and "{{env:OPENROUTER_API_KEY}}" from the container/CLI args
in python/cube.yaml to avoid leaking credentials via process arguments; instead
rely on environment-based auth by setting OPENAI_API_KEY in your env file
(replace any OPENROUTER_API_KEY usage with OPENAI_API_KEY) so the Qwen CLI
(auth-type openai) reads the key from the environment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2443407c-70ec-4003-a837-d23883d9c27d

📥 Commits

Reviewing files that changed from the base of the PR and between d6970bb and de6b6d9.

📒 Files selected for processing (1)
  • python/cube.yaml
📜 Review details
🔇 Additional comments (3)
python/cube.yaml (3)

15-15: Model alias addition is consistent.

Line [15] cleanly introduces qwen under model_aliases, aligned with the downstream CLI mapping.


36-37: Generic adapter routing is correctly wired.

Lines [36-37] map both models to generic-cli:* identifiers in a way that matches registry resolution.


124-125: Judge migration looks coherent.

Lines [124-125] switch judge_3 to qwen consistently with the new alias and CLI routing.

Comment thread python/cube.yaml
parser: "kimi"
executable: "kimi"
install_hint: "uv tool install --python 3.13 kimi-cli"
supports_resume: true
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether supports_resume is consumed by runtime code.
rg -n --type=py '\bsupports_resume\b|get\(["'\'']supports_resume["'\'']\)'

Repository: aetheronhq/agent-cube

Length of output: 605


🏁 Script executed:

# First, examine the YAML file around lines 46 and 70
sed -n '40,75p' python/cube.yaml

Repository: aetheronhq/agent-cube

Length of output: 847


🏁 Script executed:

# Search for where the YAML is loaded and how supports_resume is accessed
rg -n "supports_resume" -A 2 -B 2 --type=py | head -100

Repository: aetheronhq/agent-cube

Length of output: 2421


🏁 Script executed:

# Check GenericCLIAdapter and how it reads config
fd -t f "adapter" | grep -i generic

Repository: aetheronhq/agent-cube

Length of output: 97


🏁 Script executed:

# Look for config loading and adapter instantiation patterns
rg -n "load.*yaml|yaml.*load|GenericCLI" --type=py | head -50

Repository: aetheronhq/agent-cube

Length of output: 1199


🏁 Script executed:

# Examine GenericCLIAdapter implementation
cat -n python/cube/core/adapters/generic_cli.py

Repository: aetheronhq/agent-cube

Length of output: 4528


🏁 Script executed:

# Check how resume is gated in the adapter or runner code
rg -n "resume" --type=py -B 2 -A 2 | grep -i "supports_resume\|config\[" | head -40

Repository: aetheronhq/agent-cube

Length of output: 605


🏁 Script executed:

# Look for where the YAML config dict is accessed in adapter classes
rg -n "config\[" --type=py python/cube/core/adapters/

Repository: aetheronhq/agent-cube

Length of output: 47


🏁 Script executed:

# Search more broadly for any access to config['supports_resume'] or config.get('supports_resume')
rg -n "config.*supports_resume|supports_resume.*config" --type=py

Repository: aetheronhq/agent-cube

Length of output: 47


🏁 Script executed:

# Check if supports_resume is used in the registry or adapter base class
cat -n python/cube/core/adapters/registry.py

Repository: aetheronhq/agent-cube

Length of output: 2576


🏁 Script executed:

# Look for where GenericCLIAdapter.run() is called and how resume parameter is determined
rg -n "\.run\(.*resume" --type=py -B 3 -A 3

Repository: aetheronhq/agent-cube

Length of output: 3646


🏁 Script executed:

# Search across the codebase for any reference to the YAML supports_resume field being checked
rg -n "supports_resume" --type=py --type=yaml

Repository: aetheronhq/agent-cube

Length of output: 697


Remove unused supports_resume field from YAML configuration.

The YAML field is never read by GenericCLIAdapter or any runtime code. Resume functionality is gated by the resume parameter passed directly to adapter.run() (checking only if resume and session_id:), not by consulting this configuration field. The separate supports_resume() methods in parser classes return hardcoded values and are unrelated to the YAML field. Remove lines 46 and 70 to avoid confusion about how resume is actually controlled.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cube.yaml` at line 46, Remove the unused supports_resume YAML key:
delete the supports_resume: true entry (and the duplicate at line 70) from
python/cube.yaml because resume is controlled by the resume parameter passed to
adapter.run() and parser classes expose supports_resume() via code; ensure no
code references this YAML key remain (check for any lookups of "supports_resume"
and remove them) and keep GenericCLIAdapter behavior unchanged (resume logic
stays in adapter.run()).

Comment thread python/cube.yaml
Comment on lines +78 to +79
- "--openai-api-key"
- "{{env:OPENROUTER_API_KEY}}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For @qwen-code/qwen-code, when using --auth-type openai, which environment variables are officially supported for API key authentication, and can they replace --openai-api-key?

💡 Result:

For @qwen-code/qwen-code with --auth-type openai, the officially supported environment variables for API key authentication are OPENAI_API_KEY (primary), along with OPENAI_BASE_URL and OPENAI_MODEL for complete configuration. These can replace the --openai-api-key flag, as environment variables have the next highest priority after CLI flags according to the official documentation. The envKey in modelProviders can specify custom variables (e.g., DASHSCOPE_API_KEY), but OPENAI_API_KEY is the standard for OpenAI-compatible auth.

Citations:


🏁 Script executed:

cat -n python/cube.yaml | head -100

Repository: aetheronhq/agent-cube

Length of output: 3299


Remove command-line API key argument to prevent credential exposure.

Lines 78-79 pass the OpenRouter key via argv, which can leak through process inspection (ps, /proc/[pid]/cmdline) and operational logging.

The Qwen CLI with --auth-type openai supports environment-based authentication using the OPENAI_API_KEY variable. Remove the --openai-api-key flag and its argument—the configuration already references env files (lines 92-94) where the key should be set.

Note: Ensure the environment file contains OPENAI_API_KEY (the standard variable Qwen expects) rather than OPENROUTER_API_KEY, since Qwen CLI looks for the former when using OpenAI-compatible authentication.

Required change
       - "{{env:OPENROUTER_API_KEY}}"
       - "--openai-base-url"

Remove lines:

      - "--openai-api-key"
      - "{{env:OPENROUTER_API_KEY}}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- "--openai-api-key"
- "{{env:OPENROUTER_API_KEY}}"
- "--openai-base-url"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cube.yaml` around lines 78 - 79, Remove the command-line API key
argument entries "--openai-api-key" and "{{env:OPENROUTER_API_KEY}}" from the
container/CLI args in python/cube.yaml to avoid leaking credentials via process
arguments; instead rely on environment-based auth by setting OPENAI_API_KEY in
your env file (replace any OPENROUTER_API_KEY usage with OPENAI_API_KEY) so the
Qwen CLI (auth-type openai) reads the key from the environment.

@jacsamell jacsamell merged commit 57e83bf into main Apr 29, 2026
2 checks passed
@jacsamell jacsamell deleted the feat/generic-cli-adapter branch April 29, 2026 02:30
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