Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ raven
```

Raven supports OpenRouter, OpenAI, Anthropic, Gemini, DeepSeek, GitHub Copilot,
OpenAI Codex OAuth, and custom OpenAI-compatible endpoints.
OpenAI Codex OAuth, MiniMax Global/CN OAuth, and custom OpenAI-compatible endpoints.

If setup fails or a provider is not ready, run:

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ raven
```

Raven 支持 OpenRouter、OpenAI、Anthropic、Gemini、DeepSeek、GitHub Copilot、
OpenAI Codex OAuth,以及自定义 OpenAI-compatible endpoints。
OpenAI Codex OAuth、MiniMax Global/CN OAuth,以及自定义 OpenAI-compatible endpoints。

如果配置失败,或者 provider 还没有准备好,运行:

Expand Down
2 changes: 1 addition & 1 deletion docs/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Creates `~/.raven/config.json` and the workspace directory. Edit the config to a
| `raven gateway` | Start full server (all channels + heartbeat + cron) |
| `raven status` | Show config path, workspace, and API key status |
| `raven channels status` | Show which messaging channels are enabled |
| `raven provider login <name>` | Authenticate with an OAuth provider (e.g. `openai-codex`) |
| `raven provider login <name>` | Authenticate with an OAuth provider (for example `openai-codex`, `minimax-global`, or `minimax-cn`) |

### 6. Run tests

Expand Down
7 changes: 7 additions & 0 deletions raven/cli/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ def make_provider(config: Config):

if provider_name == "openai_codex" or model.startswith("openai-codex/"):
provider = OpenAICodexProvider(default_model=model)
elif provider_name in {"minimax_global", "minimax_cn"}:
from raven.providers.minimax_oauth_provider import MiniMaxOAuthProvider

provider = MiniMaxOAuthProvider(
region="global" if provider_name == "minimax_global" else "cn",
default_model=model,
)
elif provider_name == "azure_openai":
provider = AzureOpenAIProvider(
api_key=p.api_key,
Expand Down
131 changes: 119 additions & 12 deletions raven/cli/onboard_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ def _t(en: str, zh: str) -> str:
"label_zh": "Codex(OAuth 登录)",
"is_oauth": True,
},
{
"name": "minimax_global",
"label": "MiniMax Global (OAuth)",
"label_zh": "MiniMax Global(OAuth 登录)",
"is_oauth": True,
},
{
"name": "minimax_cn",
"label": "MiniMax CN (OAuth)",
"label_zh": "MiniMax CN(OAuth 登录)",
"is_oauth": True,
},
{
"name": "custom",
"label": "Other (OpenAI-compatible endpoint)",
Expand Down Expand Up @@ -284,10 +296,10 @@ def _load_raw_config() -> dict[str, Any]:


def _configured_providers() -> list[str]:
"""Names of providers that currently have an api_key set on disk."""
data = _load_raw_config()
providers = data.get("providers") or {}
return [name for name, p in providers.items() if isinstance(p, dict) and p.get("apiKey")]
"""Names of providers with a usable API key or OAuth token."""
from raven.config.update_providers import list_providers

return [row["name"] for row in list_providers() if row["configured"]]


def _is_config_populated() -> bool:
Expand All @@ -298,9 +310,11 @@ def _is_config_populated() -> bool:
not enough to talk to a model.
"""
data = _load_raw_config()
providers = data.get("providers") or {}
has_provider = any(isinstance(p, dict) and p.get("apiKey") for p in providers.values())
model = (data.get("agents", {}) or {}).get("defaults", {}).get("model")
configured = _configured_providers()
model_prefix = str(model or "").split("/", 1)[0].replace("-", "_")
has_non_minimax_provider = any(name not in {"minimax_global", "minimax_cn"} for name in configured)
has_provider = has_non_minimax_provider or model_prefix in configured
return bool(has_provider and model)


Expand Down Expand Up @@ -731,6 +745,11 @@ def _format_model_for_provider(spec: Any, model_id: str) -> str:
"""Apply ``spec.litellm_prefix`` to a raw ``/v1/models`` id when needed."""
if not model_id:
return model_id
if spec.name in {"minimax_global", "minimax_cn"}:
public_prefix = spec.name.replace("_", "-")
if model_id.startswith(f"{public_prefix}/"):
return model_id
return f"{public_prefix}/{model_id.split('/')[-1]}"
prefix = getattr(spec, "litellm_prefix", "") or ""
if not prefix:
return model_id
Expand Down Expand Up @@ -880,7 +899,14 @@ def _failure_choice(options: list[tuple[str, str]], *, non_interactive: bool) ->
return chosen


def _run_test_probe(provider: str, *, non_interactive: bool, warnings: list[str], allow_repick: bool = True) -> str:
def _run_test_probe(
provider: str,
*,
non_interactive: bool,
warnings: list[str],
allow_repick: bool = True,
is_oauth: bool = False,
) -> str:
"""Send a one-shot test message; on failure offer recovery options.

Returns one of ``"ok"`` / ``"continue"`` / ``"repick"`` / ``"rekey"`` /
Expand Down Expand Up @@ -910,17 +936,23 @@ def _run_test_probe(provider: str, *, non_interactive: bool, warnings: list[str]
options = [(_t("Retry", "重试"), "retry")]
if allow_repick:
options.append((_t("Re-pick model", "重新选模型"), "repick"))
options.append(
(_t("Sign in again", "重新登录"), "reauth") if is_oauth else (_t("Re-enter key", "重新填 Key"), "rekey")
)
options += [
(_t("Re-enter key", "重新填 Key"), "rekey"),
(_t("Switch provider", "更换服务商"), "switch"),
(_t("Continue anyway", "仍然继续"), "continue"),
]
choice = _failure_choice(options, non_interactive=non_interactive)
if choice == "retry":
return _run_test_probe(
provider, non_interactive=non_interactive, warnings=warnings, allow_repick=allow_repick
provider,
non_interactive=non_interactive,
warnings=warnings,
allow_repick=allow_repick,
is_oauth=is_oauth,
)
if choice in ("repick", "rekey", "switch"):
if choice in ("repick", "rekey", "reauth", "switch"):
return choice
warnings.append("provider test message")
return "continue"
Expand Down Expand Up @@ -1141,7 +1173,11 @@ def _resolve_model_with_test(
[(_t("Retry", "重试"), "retry"), (_t("Continue anyway", "仍然继续"), "continue")]
if status == "network_error"
else [
(_t("Re-enter key", "重新填 Key"), "rekey"),
(
(_t("Sign in again", "重新登录"), "reauth")
if spec.is_oauth
else (_t("Re-enter key", "重新填 Key"), "rekey")
),
(_t("Switch provider", "更换服务商"), "switch"),
(_t("Continue anyway", "仍然继续"), "continue"),
]
Expand All @@ -1152,6 +1188,10 @@ def _resolve_model_with_test(
if choice == "rekey" and not non_interactive:
_write_provider_fields(spec.name, {"api_key": _prompt_api_key(spec.name)})
continue
if choice == "reauth" and not non_interactive:
if _run_oauth_login(spec.name):
continue
return None
if choice == "switch":
return None
warnings.append("provider connectivity")
Expand Down Expand Up @@ -1187,7 +1227,12 @@ def _resolve_model_with_test(
_persist_default_model(chosen)
if skip_test:
return chosen
result = _run_test_probe(spec.name, non_interactive=non_interactive, warnings=warnings)
result = _run_test_probe(
spec.name,
non_interactive=non_interactive,
warnings=warnings,
is_oauth=spec.is_oauth,
)
if result == "switch":
return None
if result == "rekey":
Expand All @@ -1196,13 +1241,65 @@ def _resolve_model_with_test(
current = chosen
user_model_flag = None
continue
if result == "reauth":
if not _run_oauth_login(spec.name):
return None
current = chosen
user_model_flag = None
continue
if result == "repick":
current = chosen
user_model_flag = None
continue
return chosen # ok / continue


def _configure_existing_provider_model(*, non_interactive: bool) -> bool:
"""Choose a model for an already-authenticated provider without re-login."""
if non_interactive:
return False
questionary = _require_questionary()
from raven.cli._styles import RAVEN_STYLE
from raven.providers.registry import find_by_name

choices = [
questionary.Choice(_provider_label(name), value=name)
for name in _configured_providers()
if find_by_name(name) is not None
]
if not choices:
return False
provider = questionary.select(
_t("Choose the provider for the default model:", "选择默认模型对应的服务商:"),
choices=choices,
style=RAVEN_STYLE,
qmark=_QMARK,
).ask()
if not provider:
raise typer.Exit(1)
spec = find_by_name(provider)
ok, _status, model_ids = _verify_provider(provider)
if not ok:
return False
chosen = _pick_model(
spec,
current_model=None,
model_ids=model_ids,
user_provided_model=None,
non_interactive=False,
)
_persist_default_model(chosen)
result = _run_test_probe(
provider,
non_interactive=False,
warnings=[],
is_oauth=spec.is_oauth,
)
if result == "reauth":
return _run_oauth_login(provider)
return result in {"ok", "continue"}


# ---------------------------------------------------------------------------
# Step 1 — multi-provider entry (existing-config branch: done / add / edit)
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1335,6 +1432,7 @@ def _step1_provider(
),
choices=[
questionary.Choice(_t("Done, continue", "完成,继续"), value="done"),
questionary.Choice(_t("Choose default model", "选择默认模型"), value="model"),
questionary.Choice(_t("Add another provider", "新增一个服务商"), value="add"),
questionary.Choice(_t("Edit / remove a provider", "编辑 / 移除服务商"), value="edit"),
],
Expand All @@ -1355,6 +1453,15 @@ def _step1_provider(
)
continue
return None
if action == "model":
if _configure_existing_provider_model(non_interactive=False):
continue
console.print(
_t(
" [yellow]Could not configure a default model. Choose a provider and try again.[/yellow]",
" [yellow]无法配置默认模型,请重新选择服务商。[/yellow]",
)
)
if action == "add":
_configure_one_provider(
provider=None,
Expand Down
39 changes: 36 additions & 3 deletions raven/cli/provider_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Lifecycle commands:

- ``provider login <name>`` — interactive OAuth login for OAuth-based
providers (openai-codex, github-copilot)
providers (OpenAI Codex, GitHub Copilot, MiniMax Global/CN)

Config subcommands:

Expand Down Expand Up @@ -54,7 +54,7 @@ def decorator(fn):

@provider_app.command("login")
def provider_login(
provider: str = typer.Argument(..., help="OAuth provider (e.g. 'openai-codex', 'github-copilot')"),
provider: str = typer.Argument(..., help="OAuth provider (e.g. 'openai-codex', 'minimax-global')"),
):
"""Authenticate with an OAuth provider."""
from raven.providers.registry import PROVIDERS
Expand Down Expand Up @@ -123,6 +123,39 @@ async def _trigger():
raise typer.Exit(1)


def _login_minimax(region: str, label: str) -> None:
from raven.providers.minimax_oauth import login

console.print("[cyan]Starting MiniMax device flow...[/cyan]\n")
try:
token = login(
region,
print_fn=lambda message: console.print(message),
open_browser=(
bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))
if sys.platform.startswith("linux")
else True
),
)
except Exception as exc:
console.print(f"[red]Authentication error: {exc}[/red]")
raise typer.Exit(1)
if not token.access:
console.print("[red]Authentication failed[/red]")
raise typer.Exit(1)
console.print(f"[green]Authenticated with {label}[/green]")


@_register_login("minimax_global")
def _login_minimax_global() -> None:
_login_minimax("global", "MiniMax Global")


@_register_login("minimax_cn")
def _login_minimax_cn() -> None:
_login_minimax("cn", "MiniMax CN")


def _help_requested(extra_args: list[str]) -> bool:
"""Detect ``--help`` / ``-h`` inside a free-form ``ctx.args`` list."""
return any(t in ("--help", "-h") or t.startswith("--help=") for t in extra_args)
Expand Down Expand Up @@ -396,7 +429,7 @@ def provider_reset_cmd(
):
"""Restore a provider to schema defaults. Key preserved, values reset.

For OAuth providers (openai-codex, github-copilot) the on-disk token
For OAuth providers the on-disk token
file written by ``oauth_cli_kit`` is also deleted, so the user is
effectively logged out and must re-run ``provider login`` to use it.
"""
Expand Down
2 changes: 2 additions & 0 deletions raven/config/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,8 @@ class ProvidersConfig(Base):
gemini: GeminiProviderConfig = Field(default_factory=GeminiProviderConfig) # Google Gemini / Vertex AI
moonshot: ProviderConfig = Field(default_factory=ProviderConfig)
minimax: ProviderConfig = Field(default_factory=ProviderConfig)
minimax_global: ProviderConfig = Field(default_factory=ProviderConfig)
minimax_cn: ProviderConfig = Field(default_factory=ProviderConfig)
aihubmix: ProviderConfig = Field(default_factory=ProviderConfig) # AiHubMix API gateway
ollama: ProviderConfig = Field(default_factory=ProviderConfig) # Ollama local models
siliconflow: ProviderConfig = Field(default_factory=ProviderConfig) # SiliconFlow
Expand Down
Loading