feat: add generic CLI adapter and migrate Kimi#140
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughReplaces the dedicated Kimi adapter with a config-driven GenericCLIAdapter, adds Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (9)
python/cube.yamlpython/cube/core/adapters/__init__.pypython/cube/core/adapters/generic_cli.pypython/cube/core/adapters/kimi.pypython/cube/core/adapters/registry.pypython/cube/core/parsers/registry.pypython/cube/core/user_config.pytests/cli/test_adapters.pytests/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 forgeneric_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 forgeneric_cliloading.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.
GenericCLIAdapteris now properly exposed for import consumers.Also applies to: 16-16
python/cube.yaml (1)
35-35: Config migration togeneric-cli:kimilooks coherent.The new
generic_cli.kimiblock 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
.envfiles without overriding already-exported environment variables is a sensible default for local and CI runs.
| 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): |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
python/cube/core/parsers/qwen.py (1)
12-14: Consider movingimport jsonto 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 ParserAdapterThen 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
📒 Files selected for processing (6)
python/cube.yamlpython/cube/core/adapters/generic_cli.pypython/cube/core/parsers/__init__.pypython/cube/core/parsers/qwen.pypython/cube/core/parsers/registry.pytests/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
commandconfig 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
_rendermethod 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
QwenParserto 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.envfile, with proper isolation usingpatch.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 partbefore 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)) |
There was a problem hiding this comment.
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.
| 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.
| 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)) |
There was a problem hiding this comment.
"\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).
There was a problem hiding this comment.
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
📒 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
qwenundermodel_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_3toqwenconsistently with the new alias and CLI routing.
| parser: "kimi" | ||
| executable: "kimi" | ||
| install_hint: "uv tool install --python 3.13 kimi-cli" | ||
| supports_resume: true |
There was a problem hiding this comment.
🧩 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.yamlRepository: 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 -100Repository: aetheronhq/agent-cube
Length of output: 2421
🏁 Script executed:
# Check GenericCLIAdapter and how it reads config
fd -t f "adapter" | grep -i genericRepository: 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 -50Repository: aetheronhq/agent-cube
Length of output: 1199
🏁 Script executed:
# Examine GenericCLIAdapter implementation
cat -n python/cube/core/adapters/generic_cli.pyRepository: 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 -40Repository: 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=pyRepository: 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.pyRepository: 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 3Repository: 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=yamlRepository: 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()).
| - "--openai-api-key" | ||
| - "{{env:OPENROUTER_API_KEY}}" |
There was a problem hiding this comment.
🧩 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:
- 1: https://github.com/QwenLM/qwen-code/blob/a0a0a70b/docs/users/configuration/auth.md
- 2: https://github.com/QwenLM/qwen-code/blob/407a66c9/docs/users/configuration/model-providers.md
- 3: https://qwenlm.github.io/qwen-code-docs/en/users/configuration/auth/
- 4: https://qwenlm.github.io/qwen-code-docs/en/users/configuration/model-providers/
- 5: https://github.com/QwenLM/qwen-code/tree/5cd33497738a0d8187af8f1503fab91f6be18638
- 6: https://github.com/QwenLM/qwen-code/blob/964509f58736f97382b9053201f4971e9b370249/README.md
- 7: fix(cli): honor --openai-api-key in non-interactive auth validation QwenLM/qwen-code#3187
🏁 Script executed:
cat -n python/cube.yaml | head -100Repository: 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.
| - "--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.
Summary
GenericCLIAdapterfor command-template CLI integrations.kimi_k26_openroutertogeneric-cli:kimiwhile keepingKimiParserexplicit.generic-cli:<name>.Test plan
python -m pytest tests/cli/test_adapters.py tests/core/test_user_config.py -qpython -m pytest tests/ -qget_adapter("generic-cli:kimi")withkimi_k26_openrouterThis 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
Configuration
Registry & parsers
Removals & exports
Tests
Why it matters
Testing