Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

257 lines
9.1 KiB
Python

"""Live provider validation and onboarding demo helpers."""
from __future__ import annotations
import os
from dataclasses import dataclass
from typing import Any
from surfaces.cli.wizard.config import ProviderOption
Anthropic: Any | None = None
AnthropicAuthError: type[Exception] | None = None
OpenAI: Any | None = None
OpenAIAuthError: type[Exception] | None = None
def _load_anthropic_client() -> tuple[Any, type[Exception]]:
global Anthropic, AnthropicAuthError
if Anthropic is None or AnthropicAuthError is None:
from anthropic import Anthropic as _Anthropic
from anthropic import AuthenticationError as _AnthropicAuthError
Anthropic = _Anthropic
AnthropicAuthError = _AnthropicAuthError
return Anthropic, AnthropicAuthError
def _load_openai_client() -> tuple[Any, type[Exception]]:
global OpenAI, OpenAIAuthError
if OpenAI is None or OpenAIAuthError is None:
from openai import AuthenticationError as _OpenAIAuthError
from openai import OpenAI as _OpenAI
OpenAI = _OpenAI
OpenAIAuthError = _OpenAIAuthError
return OpenAI, OpenAIAuthError
@dataclass(frozen=True)
class ValidationResult:
"""Result of validating a provider key."""
ok: bool
detail: str
sample_response: str = ""
def _get_provider_base_url(provider_value: str) -> str | None:
"""Get the base_url for OpenAI-compatible non-OpenAI providers, or None for native OpenAI."""
if provider_value == "openrouter":
from config.config import OPENROUTER_BASE_URL
return OPENROUTER_BASE_URL
if provider_value == "deepseek":
from config.config import DEEPSEEK_BASE_URL
return DEEPSEEK_BASE_URL
if provider_value == "gemini":
from config.config import GEMINI_BASE_URL
return GEMINI_BASE_URL
if provider_value == "nvidia":
from config.config import NVIDIA_BASE_URL
return NVIDIA_BASE_URL
if provider_value == "groq":
from config.config import GROQ_BASE_URL
return GROQ_BASE_URL
return None
def _provider_validation_label(provider: ProviderOption) -> str:
suffix = " API key"
if provider.label.endswith(suffix):
return provider.label[: -len(suffix)]
return provider.label
def _check_azure_openai(
*,
api_key: str,
model: str,
base_url: str,
api_version: str,
) -> ValidationResult:
"""Validate Azure OpenAI credentials with a tiny chat completion."""
from core.llm.providers.azure_openai import normalize_azure_openai_base_url
normalized_base = normalize_azure_openai_base_url(base_url)
if not normalized_base:
return ValidationResult(
ok=False,
detail="Azure OpenAI resource URL is missing. Set AZURE_OPENAI_BASE_URL.",
)
from core.llm.providers.azure_openai import resolve_azure_openai_api_version
resolved_api_version = resolve_azure_openai_api_version(api_version)
openai_client_cls, openai_auth_error = _load_openai_client()
azure_base = f"{normalized_base}/openai/deployments/{model}"
try:
client = openai_client_cls(
api_key=api_key,
base_url=azure_base,
default_query={"api-version": resolved_api_version},
timeout=30.0,
)
request_kwargs: dict[str, object] = {
"model": model,
"messages": [{"role": "user", "content": "Reply with exactly: OpenSRE ready"}],
}
if model.startswith(("o1", "o3", "o4", "gpt-5")):
request_kwargs["max_completion_tokens"] = 24
else:
request_kwargs["max_tokens"] = 24
response = client.chat.completions.create(**request_kwargs)
sample_text = (response.choices[0].message.content or "").strip()
return ValidationResult(
ok=True,
detail="Azure OpenAI API key validated.",
sample_response=sample_text,
)
except openai_auth_error:
return ValidationResult(ok=False, detail="Azure OpenAI rejected the API key.")
except Exception as err:
return ValidationResult(ok=False, detail=f"Validation request failed: {err}")
def _check_ollama(host: str, model: str) -> ValidationResult:
"""Check Ollama server connectivity and verify model responds to inference."""
import httpx
tags_url = f"{host.rstrip('/')}/api/tags"
try:
r = httpx.get(tags_url, timeout=5.0)
r.raise_for_status()
except Exception as err:
return ValidationResult(
ok=False,
detail=f"Cannot reach Ollama at {host}. Is it running? Try: ollama serve\n({err})",
)
available = [m["name"] for m in r.json().get("models", [])]
from surfaces.cli.wizard.local_llm.ollama import normalize_model_tag
normalized_model = normalize_model_tag(model)
base_name = model.split(":")[0]
matched = normalized_model in available or any(m.split(":")[0] == base_name for m in available)
if not matched:
listed = ", ".join(available) or "none pulled yet"
return ValidationResult(
ok=False,
detail=f"Model '{model}' not found. Run: ollama pull {model}\nAvailable: {listed}",
)
# Verify the model actually responds to an inference request
chat_url = f"{host.rstrip('/')}/v1/chat/completions"
try:
resp = httpx.post(
chat_url,
json={
"model": model,
"messages": [{"role": "user", "content": "Reply with exactly: OpenSRE ready"}],
"max_tokens": 24,
},
timeout=60.0,
)
resp.raise_for_status()
sample_text = (resp.json()["choices"][0]["message"]["content"] or "").strip()
except Exception as err:
return ValidationResult(
ok=False,
detail=f"Model '{model}' is pulled but failed to respond: {err}",
)
return ValidationResult(
ok=True, detail=f"Ollama reachable. Model '{model}' is ready.", sample_response=sample_text
)
def validate_provider_credentials(
*,
provider: ProviderOption,
api_key: str,
model: str,
) -> ValidationResult:
"""Run a tiny live request against the selected provider."""
if provider.value == "ollama":
return _check_ollama(host=api_key, model=model)
if provider.value == "azure-openai":
return _check_azure_openai(
api_key=api_key,
model=model,
base_url=os.getenv(provider.endpoint_env, "").strip(),
api_version=os.getenv(provider.api_version_env, "").strip(),
)
anthropic_client_cls, anthropic_auth_error = _load_anthropic_client()
openai_client_cls, openai_auth_error = _load_openai_client()
try:
if provider.value == "anthropic":
anthropic_client = anthropic_client_cls(api_key=api_key, timeout=30.0)
anthropic_response = anthropic_client.messages.create(
model=model,
max_tokens=24,
messages=[{"role": "user", "content": "Reply with exactly: OpenSRE ready"}],
)
sample_text = "".join(
block.text
for block in getattr(anthropic_response, "content", [])
if getattr(block, "type", None) == "text"
).strip()
return ValidationResult(
ok=True, detail="Anthropic API key validated.", sample_response=sample_text
)
# All OpenAI-compatible providers (openai, openrouter, deepseek, gemini, nvidia)
base_url = _get_provider_base_url(provider.value)
openai_client = openai_client_cls(api_key=api_key, base_url=base_url, timeout=30.0)
# Only native OpenAI reasoning models use max_completion_tokens; others use max_tokens
if provider.value == "openai" and model.startswith(("o1", "o3", "o4", "gpt-5")):
openai_response = openai_client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": "Reply with exactly: OpenSRE ready"}],
max_completion_tokens=24,
)
else:
openai_response = openai_client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": "Reply with exactly: OpenSRE ready"}],
max_tokens=24,
)
sample_text = (openai_response.choices[0].message.content or "").strip()
provider_label = _provider_validation_label(provider)
return ValidationResult(
ok=True, detail=f"{provider_label} API key validated.", sample_response=sample_text
)
except anthropic_auth_error:
return ValidationResult(ok=False, detail="Anthropic rejected the API key.")
except openai_auth_error:
return ValidationResult(
ok=False, detail=f"{_provider_validation_label(provider)} rejected the API key."
)
except Exception as err:
return ValidationResult(ok=False, detail=f"Validation request failed: {err}")
def build_demo_action_response() -> dict:
"""Return a safe built-in action response for onboarding."""
from tools.system.sre_guidance_tool import get_sre_guidance
return get_sre_guidance(topic="recovery_remediation", max_topics=1)