{"repo": "adamhuang17/agenthub", "files": {"apps/web/packages/opencode/src/session/retry.ts": {"a24b2d45fca04539ad84833fd5bf2208044323a0": {"t": 1785596399.9764242, "n": 7152, "h": "0c7fcace557156bd", "c": "import type { NamedError } from \"@opencode-ai/core/util/error\"\nimport { SessionV1 } from \"@opencode-ai/core/v1/session\"\nimport { Cause, Clock, Duration, Effect, Schedule } from \"effect\"\nimport { MessageV2 } from \"./message-v2\"\nimport { iife } from \"@/util/iife\"\nimport { isRecord } from \"@/util/record\"\n\nexport type Err = ReturnType\n\nexport const GO_UPSELL_MESSAGE = \"Free usage exceeded, subscribe to Go\"\nexport const GO_UPSELL_URL = \"https://opencode.ai/go\"\nexport type RetryReason = \"free_tier_limit\" | \"account_rate_limit\" | (string & {})\n\nexport type Retryable = {\n message: string\n action?: {\n reason: RetryReason\n provider: string\n title: string\n message: string\n label: string\n link?: string\n }\n}\n\nexport const RETRY_INITIAL_DELAY = 2000\nexport const RETRY_BACKOFF_FACTOR = 2\nexport const RETRY_MAX_DELAY_NO_HEADERS = 30_000 // 30 seconds\nexport const RETRY_MAX_DELAY = 2_147_483_647 // max 32-bit signed integer for setTimeout\n\nfunction cap(ms: number) {\n return Math.min(ms, RETRY_MAX_DELAY)\n}\n\nexport function delay(attempt: number, error?: SessionV1.APIError) {\n if (error) {\n const headers = error.data.responseHeaders\n if (headers) {\n const retryAfterMs = headers[\"retry-after-ms\"]\n if (retryAfterMs) {\n const parsedMs = Number.parseFloat(retryAfterMs)\n if (!Number.isNaN(parsedMs)) {\n return cap(parsedMs)\n }\n }\n\n const retryAfter = headers[\"retry-after\"]\n if (retryAfter) {\n const parsedSeconds = Number.parseFloat(retryAfter)\n if (!Number.isNaN(parsedSeconds)) {\n // convert seconds to milliseconds\n return cap(Math.ceil(parsedSeconds * 1000))\n }\n // Try parsing as HTTP date format\n const parsed = Date.parse(retryAfter) - Date.now()\n if (!Number.isNaN(parsed) && parsed > 0) {\n return cap(Math.ceil(parsed))\n }\n }\n\n return cap(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1))\n }\n }\n\n return cap(Math.min(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1), RETRY_MAX_DELAY_NO_HEADERS))\n}\n\nexport function retryable(error: Err, provider: string) {\n // context overflow errors should not be retried\n if (SessionV1.ContextOverflowError.isInstance(error)) return undefined\n if (SessionV1.APIError.isInstance(error)) {\n const status = error.data.statusCode\n // 5xx errors are transient server failures and should always be retried,\n // even when the provider SDK doesn't explicitly mark them as retryable.\n if (!error.data.isRetryable && !(status !== undefined && status >= 500)) return undefined\n if (error.data.responseBody?.includes(\"FreeUsageLimitError\")) {\n return {\n message: GO_UPSELL_MESSAGE,\n action: {\n reason: \"free_tier_limit\",\n provider,\n title: \"Free limit reached\",\n message: \"Subscribe to OpenCode Go for reliable access to the best open-source models, starting at $5/month.\",\n label: \"subscribe\",\n link: GO_UPSELL_URL,\n },\n }\n }\n if (error.data.responseBody?.includes(\"GoUsageLimitError\")) {\n const body = parseJSON(error.data.responseBody)\n const workspace = str(body?.metadata?.workspace)\n const limitName = str(body?.metadata?.limitName)\n const retryAfter = num(error.data.responseHeaders?.[\"retry-after\"])\n const resetIn = iife(() => {\n if (retryAfter === undefined) return \"\"\n const seconds = Math.max(0, Math.ceil(retryAfter))\n const days = Math.floor(seconds / 86_400)\n const hours = Math.floor((seconds % 86_400) / 3_600)\n const minutes = Math.ceil((seconds % 3_600) / 60)\n const unit = (value: number, name: string) => `${value} ${name}${value === 1 ? \"\" : \"s\"}`\n\n if (days > 0) return hours > 0 ? `${unit(days, \"day\")} ${unit(hours, \"hour\")}` : unit(days, \"day\")\n if (hours > 0) return minutes > 0 ? `${unit(hours, \"hour\")} ${unit(minutes, \"minute\")}` : unit(hours, \"hour\")\n return minutes > 0 ? unit(minutes, \"minute\") : \"less than a minute\"\n })\n\n const message = `${limitName ? `${limitName} usage limit` : \"Usage limit\"} reached. It will reset in ${resetIn}. To continue using this model now, enable usage from your available balance`\n\n const link = `https://opencode.ai/workspace/${workspace}/go`\n return {\n message: `${message} - ${link}`,\n action: {\n reason: \"account_rate_limit\",\n provider,\n title: \"Go limit reached\",\n message,\n label: \"open settings\",\n link,\n },\n }\n }\n return { message: error.data.message.includes(\"Overloaded\") ? \"Provider is overloaded\" : error.data.message }\n }\n\n // Check for rate limit patterns in plain text error messages\n const msg = isRecord(error.data) ? error.data.message : undefined\n if (typeof msg === \"string\") {\n const lower = msg.toLowerCase()\n if (\n lower.includes(\"rate increased too quickly\") ||\n lower.includes(\"rate limit\") ||\n lower.includes(\"too many requests\")\n ) {\n return { message: msg }\n }\n }\n\n const json = parseJSON(msg)\n if (!json || typeof json !== \"object\") return undefined\n const code = typeof json.code === \"string\" ? json.code : \"\"\n\n if (json.type === \"error\" && json.error?.type === \"too_many_requests\") {\n return { message: \"Too Many Requests\" }\n }\n if (code.includes(\"exhausted\") || code.includes(\"unavailable\")) {\n return { message: \"Provider is overloaded\" }\n }\n if (json.type === \"error\" && typeof json.error?.code === \"string\" && json.error.code.includes(\"rate_limit\")) {\n return { message: \"Rate Limited\" }\n }\n return undefined\n}\n\nfunction str(value: unknown) {\n if (value === undefined || value === null) return \"\"\n return String(value)\n}\n\nfunction num(value: unknown) {\n const parsed = Number.parseFloat(str(value))\n if (Number.isNaN(parsed)) return undefined\n return parsed\n}\n\nfunction parseJSON(value: unknown) {\n return iife(() => {\n try {\n if (typeof value !== \"string\") return undefined\n return JSON.parse(value)\n } catch {\n return undefined\n }\n })\n}\n\nexport function policy(opts: {\n provider: string\n parse: (error: unknown) => Err\n set: (input: { attempt: number; message: string; action?: Retryable[\"action\"]; next: number }) => Effect.Effect\n}) {\n return Schedule.fromStepWithMetadata(\n Effect.succeed((meta: Schedule.InputMetadata) => {\n const error = opts.parse(meta.input)\n const retry = retryable(error, opts.provider)\n if (!retry) return Cause.done(meta.attempt)\n return Effect.gen(function* () {\n const wait = delay(meta.attempt, SessionV1.APIError.isInstance(error) ? error : undefined)\n const now = yield* Clock.currentTimeMillis\n yield* opts.set({\n attempt: meta.attempt,\n message: retry.message,\n action: retry.action,\n next: now + wait,\n })\n return [meta.attempt, Duration.millis(wait)] as [number, Duration.Duration]\n })\n }),\n )\n}\n\nexport * as SessionRetry from \"./retry\"\n"}}, "services/api/app/agents/provider_config.py": {"a24b2d45fca04539ad84833fd5bf2208044323a0": {"t": 1785746893.4640043, "n": 25267, "h": "87bef06ab0793168", "c": "from __future__ import annotations\n\nimport os\nimport re\nimport shutil\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Any\n\nfrom services.api.app.shared.errors import ValidationError\nfrom services.api.app.shared.settings import get_settings\n\n\nBACKEND_TYPES = {\"model_agent_backend\", \"coding_agent_backend\", \"tool_integration\"}\nADAPTER_KINDS = {\"custom_openai\", \"codex_cli\", \"claude_code_cli\", \"opencode_http\", \"disabled\"}\nWORKSPACE_MODES = {\"none\", \"read_only\", \"readonly_chat\", \"workspace_write\"}\nHEALTH_CHECK_STRATEGIES = {\"direct_probe\", \"metadata_probe\", \"none\"}\n\n_ENV_NAME_RE = re.compile(r\"^[A-Z_][A-Z0-9_]*$\")\n_CREDENTIAL_REF_RE = re.compile(r\"^credential_ref:[A-Za-z0-9_.:/-]+$\")\nCUSTOM_OPENAI_PROVIDER_KEYS = (\n \"qwen_turbo\",\n \"volc_deepseek_flash\",\n \"volc_deepseek_pro\",\n \"deepseek_official\",\n)\n\n_PROVIDER_DEFAULTS: dict[str, dict[str, object]] = {\n \"custom_openai\": {\n \"adapter_kind\": \"custom_openai\",\n \"credential_env\": [\"AGENTHUB_CUSTOM_OPENAI_API_KEY\", \"CUSTOM_OPENAI_API_KEY\"],\n \"credential_source\": \"AGENTHUB_CUSTOM_OPENAI_API_KEY\",\n },\n \"openai\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\"AGENTHUB_OPENAI_API_BASE\", \"OPENAI_API_BASE\", \"OPENAI_BASE_URL\"],\n \"model_env\": [\"AGENTHUB_OPENAI_MODEL\", \"OPENAI_MODEL\"],\n \"credential_source\": \"OPENAI_API_KEY\",\n },\n \"deepseek\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\n \"AGENTHUB_PROVIDER_DEEPSEEK_API_BASE\",\n \"AGENTHUB_DEEPSEEK_API_BASE\",\n \"DEEPSEEK_API_BASE\",\n \"AGENTHUB_CUSTOM_OPENAI_API_BASE\",\n ],\n \"model_env\": [\n \"AGENTHUB_PROVIDER_DEEPSEEK_MODEL\",\n \"AGENTHUB_DEEPSEEK_MODEL\",\n \"DEEPSEEK_MODEL\",\n \"AGENTHUB_CUSTOM_OPENAI_MODEL\",\n ],\n \"credential_env\": [\n \"AGENTHUB_PROVIDER_DEEPSEEK_API_KEY\",\n \"AGENTHUB_DEEPSEEK_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"AGENTHUB_CUSTOM_OPENAI_API_KEY\",\n ],\n \"credential_source\": \"DEEPSEEK_API_KEY\",\n },\n \"zhipu\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\"AGENTHUB_ZHIPU_API_BASE\", \"ZHIPU_API_BASE\"],\n \"model_env\": [\"AGENTHUB_ZHIPU_MODEL\", \"ZHIPU_MODEL\"],\n \"credential_source\": \"ZHIPU_API_KEY\",\n },\n \"doubao\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\"AGENTHUB_DOUBAO_API_BASE\", \"DOUBAO_API_BASE\"],\n \"model_env\": [\"AGENTHUB_DOUBAO_MODEL\", \"DOUBAO_MODEL\"],\n \"credential_source\": \"DOUBAO_API_KEY\",\n },\n \"qwen\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\n \"AGENTHUB_PROVIDER_QWEN_API_BASE\",\n \"AGENTHUB_QWEN_TURBO_API_BASE\",\n \"AGENTHUB_QWEN_API_BASE\",\n \"AGENTHUB_CUSTOM_OPENAI_API_BASE\",\n ],\n \"model_env\": [\n \"AGENTHUB_PROVIDER_QWEN_MODEL\",\n \"AGENTHUB_QWEN_TURBO_MODEL\",\n \"AGENTHUB_QWEN_MODEL\",\n \"AGENTHUB_CUSTOM_OPENAI_MODEL\",\n ],\n \"credential_env\": [\n \"AGENTHUB_PROVIDER_QWEN_API_KEY\",\n \"AGENTHUB_QWEN_TURBO_API_KEY\",\n \"AGENTHUB_QWEN_API_KEY\",\n \"AGENTHUB_CUSTOM_OPENAI_API_KEY\",\n ],\n \"credential_source\": \"QWEN_API_KEY\",\n },\n \"qwen_turbo\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\n \"AGENTHUB_PROVIDER_QWEN_API_BASE\",\n \"AGENTHUB_QWEN_TURBO_API_BASE\",\n \"AGENTHUB_QWEN_API_BASE\",\n \"AGENTHUB_CUSTOM_OPENAI_API_BASE\",\n ],\n \"model_env\": [\n \"AGENTHUB_PROVIDER_QWEN_MODEL\",\n \"AGENTHUB_QWEN_TURBO_MODEL\",\n \"AGENTHUB_QWEN_MODEL\",\n \"AGENTHUB_CUSTOM_OPENAI_MODEL\",\n ],\n \"credential_env\": [\n \"AGENTHUB_PROVIDER_QWEN_API_KEY\",\n \"AGENTHUB_QWEN_TURBO_API_KEY\",\n \"AGENTHUB_QWEN_API_KEY\",\n \"AGENTHUB_CUSTOM_OPENAI_API_KEY\",\n ],\n \"credential_source\": \"AGENTHUB_PROVIDER_QWEN_API_KEY\",\n \"strict_env\": True,\n },\n \"volc_deepseek_flash\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\"AGENTHUB_PROVIDER_VOLC_DEEPSEEK_API_BASE\"],\n \"model_env\": [\"AGENTHUB_PROVIDER_VOLC_DEEPSEEK_FLASH_MODEL\"],\n \"credential_source\": \"AGENTHUB_PROVIDER_VOLC_DEEPSEEK_API_KEY\",\n \"strict_env\": True,\n },\n \"volc_deepseek_pro\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\"AGENTHUB_PROVIDER_VOLC_DEEPSEEK_API_BASE\"],\n \"model_env\": [\"AGENTHUB_PROVIDER_VOLC_DEEPSEEK_PRO_MODEL\"],\n \"credential_source\": \"AGENTHUB_PROVIDER_VOLC_DEEPSEEK_API_KEY\",\n \"strict_env\": True,\n },\n \"deepseek_official\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\n \"AGENTHUB_PROVIDER_DEEPSEEK_API_BASE\",\n \"AGENTHUB_DEEPSEEK_API_BASE\",\n \"DEEPSEEK_API_BASE\",\n \"AGENTHUB_CUSTOM_OPENAI_API_BASE\",\n ],\n \"model_env\": [\n \"AGENTHUB_PROVIDER_DEEPSEEK_MODEL\",\n \"AGENTHUB_DEEPSEEK_MODEL\",\n \"DEEPSEEK_MODEL\",\n \"AGENTHUB_CUSTOM_OPENAI_MODEL\",\n ],\n \"credential_env\": [\n \"AGENTHUB_PROVIDER_DEEPSEEK_API_KEY\",\n \"AGENTHUB_DEEPSEEK_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"AGENTHUB_CUSTOM_OPENAI_API_KEY\",\n ],\n \"credential_source\": \"AGENTHUB_PROVIDER_DEEPSEEK_API_KEY\",\n \"strict_env\": True,\n },\n \"openrouter\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\"AGENTHUB_OPENROUTER_API_BASE\", \"OPENROUTER_API_BASE\"],\n \"model_env\": [\"AGENTHUB_OPENROUTER_MODEL\", \"OPENROUTER_MODEL\"],\n \"credential_source\": \"OPENROUTER_API_KEY\",\n },\n \"ollama\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\"AGENTHUB_OLLAMA_API_BASE\", \"OLLAMA_API_BASE\"],\n \"model_env\": [\"AGENTHUB_OLLAMA_MODEL\", \"OLLAMA_MODEL\"],\n \"credential_source\": \"OLLAMA_API_KEY\",\n },\n \"codex\": {\n \"adapter_kind\": \"codex_cli\",\n \"executable_env\": [\"AGENTHUB_CODEX_EXECUTABLE\", \"CODEX_EXECUTABLE\", \"AGENTHUB_CODEX_CLI_EXECUTABLE\"],\n \"executable_path\": \"codex\",\n },\n \"anthropic\": {\n \"adapter_kind\": \"claude_code_cli\",\n \"executable_env\": [\"AGENTHUB_CLAUDE_CODE_EXECUTABLE\", \"AGENTHUB_CLAUDE_EXECUTABLE\", \"CLAUDE_CODE_EXECUTABLE\"],\n \"executable_path\": \"claude\",\n },\n \"opencode\": {\n \"adapter_kind\": \"opencode_http\",\n \"api_base_env\": [\"OPENCODE_API_BASE\", \"AGENTHUB_OPENCODE_API_BASE\"],\n },\n # Chinese provider name aliases – map to the same env-variable chains as\n # their English counterparts so that runtime resolution works regardless of\n # which name the agent profile stores.\n \"深度求索\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\n \"AGENTHUB_PROVIDER_DEEPSEEK_API_BASE\",\n \"AGENTHUB_DEEPSEEK_API_BASE\",\n \"DEEPSEEK_API_BASE\",\n \"AGENTHUB_CUSTOM_OPENAI_API_BASE\",\n ],\n \"model_env\": [\n \"AGENTHUB_PROVIDER_DEEPSEEK_MODEL\",\n \"AGENTHUB_DEEPSEEK_MODEL\",\n \"DEEPSEEK_MODEL\",\n \"AGENTHUB_CUSTOM_OPENAI_MODEL\",\n ],\n \"credential_env\": [\n \"AGENTHUB_PROVIDER_DEEPSEEK_API_KEY\",\n \"AGENTHUB_DEEPSEEK_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"AGENTHUB_CUSTOM_OPENAI_API_KEY\",\n ],\n \"credential_source\": \"DEEPSEEK_API_KEY\",\n },\n \"阿里千问\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\n \"AGENTHUB_PROVIDER_QWEN_API_BASE\",\n \"AGENTHUB_QWEN_TURBO_API_BASE\",\n \"AGENTHUB_QWEN_API_BASE\",\n \"AGENTHUB_CUSTOM_OPENAI_API_BASE\",\n ],\n \"model_env\": [\n \"AGENTHUB_PROVIDER_QWEN_MODEL\",\n \"AGENTHUB_QWEN_TURBO_MODEL\",\n \"AGENTHUB_QWEN_MODEL\",\n \"AGENTHUB_CUSTOM_OPENAI_MODEL\",\n ],\n \"credential_env\": [\n \"AGENTHUB_PROVIDER_QWEN_API_KEY\",\n \"AGENTHUB_QWEN_TURBO_API_KEY\",\n \"AGENTHUB_QWEN_API_KEY\",\n \"AGENTHUB_CUSTOM_OPENAI_API_KEY\",\n ],\n \"credential_source\": \"QWEN_API_KEY\",\n },\n \"千问\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\n \"AGENTHUB_PROVIDER_QWEN_API_BASE\",\n \"AGENTHUB_QWEN_TURBO_API_BASE\",\n \"AGENTHUB_QWEN_API_BASE\",\n \"AGENTHUB_CUSTOM_OPENAI_API_BASE\",\n ],\n \"model_env\": [\n \"AGENTHUB_PROVIDER_QWEN_MODEL\",\n \"AGENTHUB_QWEN_TURBO_MODEL\",\n \"AGENTHUB_QWEN_MODEL\",\n \"AGENTHUB_CUSTOM_OPENAI_MODEL\",\n ],\n \"credential_env\": [\n \"AGENTHUB_PROVIDER_QWEN_API_KEY\",\n \"AGENTHUB_QWEN_TURBO_API_KEY\",\n \"AGENTHUB_QWEN_API_KEY\",\n \"AGENTHUB_CUSTOM_OPENAI_API_KEY\",\n ],\n \"credential_source\": \"QWEN_API_KEY\",\n },\n \"智谱\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\"AGENTHUB_ZHIPU_API_BASE\", \"ZHIPU_API_BASE\"],\n \"model_env\": [\"AGENTHUB_ZHIPU_MODEL\", \"ZHIPU_MODEL\"],\n \"credential_source\": \"ZHIPU_API_KEY\",\n },\n \"豆包\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\"AGENTHUB_DOUBAO_API_BASE\", \"DOUBAO_API_BASE\"],\n \"model_env\": [\"AGENTHUB_DOUBAO_MODEL\", \"DOUBAO_MODEL\"],\n \"credential_source\": \"DOUBAO_API_KEY\",\n },\n}\n\n\n@dataclass(frozen=True)\nclass ProviderConfig:\n provider: str | None\n adapter_kind: str\n backend_type: str\n api_base: str | None\n model: str | None\n credential_source: str | dict[str, str] | None\n executable_path: str | None\n timeout_seconds: int\n max_output_tokens: int\n temperature: float\n workspace_mode: str\n allowed_tools: list[str]\n health_check_strategy: str\n\n\ndef validate_provider_config(raw: dict[str, Any]) -> ProviderConfig:\n if not isinstance(raw, dict):\n raise ValidationError(\"ProviderConfig must be an object.\", code=\"provider_config_invalid\")\n\n provider = _optional_clean_string(raw.get(\"provider\"), \"provider\")\n adapter_kind = _enum(raw, \"adapter_kind\", ADAPTER_KINDS)\n backend_type = _enum(raw, \"backend_type\", BACKEND_TYPES)\n _validate_adapter_backend_pair(adapter_kind, backend_type)\n\n config = ProviderConfig(\n provider=provider,\n adapter_kind=adapter_kind,\n backend_type=backend_type,\n api_base=_optional_clean_string(raw.get(\"api_base\"), \"api_base\"),\n model=_optional_clean_string(raw.get(\"model\"), \"model\"),\n credential_source=_credential_source(raw.get(\"credential_source\")),\n executable_path=_optional_clean_string(raw.get(\"executable_path\"), \"executable_path\"),\n timeout_seconds=_positive_int(raw.get(\"timeout_seconds\"), \"timeout_seconds\", default=30),\n max_output_tokens=_positive_int(raw.get(\"max_output_tokens\"), \"max_output_tokens\", default=4096),\n temperature=_temperature(raw.get(\"temperature\")),\n workspace_mode=_enum(raw, \"workspace_mode\", WORKSPACE_MODES),\n allowed_tools=_string_list(raw.get(\"allowed_tools\"), \"allowed_tools\"),\n health_check_strategy=_enum(raw, \"health_check_strategy\", HEALTH_CHECK_STRATEGIES),\n )\n _reject_secret_like_config(config)\n return config\n\n\ndef provider_config_from_agent(agent_profile: dict[str, object]) -> ProviderConfig:\n provider = _provider(agent_profile)\n defaults = _PROVIDER_DEFAULTS.get(provider or \"\", {})\n adapter_kind = _profile_string(agent_profile, \"adapter_kind\") or str(defaults.get(\"adapter_kind\") or \"disabled\")\n allow_generic_env = defaults.get(\"strict_env\") is not True\n\n raw = {\n \"provider\": provider,\n \"adapter_kind\": adapter_kind,\n \"backend_type\": _backend_type_for_adapter(adapter_kind),\n \"api_base\": _profile_string(agent_profile, \"api_base\")\n or _first_env(defaults.get(\"api_base_env\"))\n or _first_env([f\"AGENTHUB_{_env_key(provider)}_API_BASE\"] if provider and allow_generic_env else None),\n \"model\": _profile_string(agent_profile, \"model\")\n or _first_env(defaults.get(\"model_env\"))\n or _first_env([f\"AGENTHUB_{_env_key(provider)}_MODEL\"] if provider and allow_generic_env else None),\n \"credential_source\": _profile_credential_source(agent_profile)\n or _first_env([f\"AGENTHUB_{_env_key(provider)}_CREDENTIAL_SOURCE\"] if provider and allow_generic_env else None)\n or _first_env_name(defaults.get(\"credential_env\"))\n or defaults.get(\"credential_source\"),\n \"executable_path\": _profile_string(agent_profile, \"executable_path\")\n or _first_executable_env(defaults.get(\"executable_env\"))\n or defaults.get(\"executable_path\"),\n \"timeout_seconds\": _profile_int(agent_profile, \"timeout_seconds\")\n or _first_env_int([f\"AGENTHUB_{_env_key(provider)}_TIMEOUT_SECONDS\"] if provider else None)\n or 30,\n \"max_output_tokens\": _profile_int(agent_profile, \"max_output_tokens\")\n or _first_env_int([f\"AGENTHUB_{_env_key(provider)}_MAX_OUTPUT_TOKENS\"] if provider else None)\n or 4096,\n \"temperature\": _profile_float(agent_profile, \"temperature\")\n or _first_env_float([f\"AGENTHUB_{_env_key(provider)}_TEMPERATURE\"] if provider else None)\n or 0.2,\n \"workspace_mode\": _profile_string(agent_profile, \"workspace_mode\")\n or (\n \"read_only\"\n if adapter_kind in {\"codex_cli\", \"claude_code_cli\"}\n else \"readonly_chat\"\n if adapter_kind == \"custom_openai\"\n else \"none\"\n ),\n \"allowed_tools\": _profile_string_list(agent_profile, \"allowed_tools\"),\n \"health_check_strategy\": _profile_string(agent_profile, \"health_check_strategy\") or \"direct_probe\",\n }\n return validate_provider_config(raw)\n\n\ndef custom_openai_provider_config(provider_key: str) -> ProviderConfig:\n clean = provider_key.strip().lower() if isinstance(provider_key, str) else \"\"\n if clean not in CUSTOM_OPENAI_PROVIDER_KEYS:\n raise ValidationError(\n f\"Unsupported A7-3.1 custom_openai provider: {provider_key}\",\n code=\"provider_config_invalid_provider\",\n )\n return provider_config_from_agent(\n {\n \"provider\": clean,\n \"adapter_kind\": \"custom_openai\",\n \"execution_enabled\": True,\n \"configured\": True,\n \"health_status\": \"configured\",\n }\n )\n\n\ndef custom_openai_provider_configs_from_environment() -> dict[str, ProviderConfig]:\n return {provider_key: custom_openai_provider_config(provider_key) for provider_key in CUSTOM_OPENAI_PROVIDER_KEYS}\n\n\ndef credential_env_name(config: ProviderConfig) -> str | None:\n if isinstance(config.credential_source, str) and _ENV_NAME_RE.match(config.credential_source):\n return config.credential_source\n return None\n\n\ndef credential_value_from_environment(config: ProviderConfig) -> str | None:\n if isinstance(config.credential_source, str) and config.credential_source.startswith(\"credential_ref:agent:\"):\n return _credential_value_from_agent_ref(config.credential_source)\n env_name = credential_env_name(config)\n if env_name is None:\n return None\n value = _env_value(env_name)\n return value if isinstance(value, str) and value.strip() else None\n\n\ndef _credential_value_from_agent_ref(source: str) -> str | None:\n parts = source.split(\":\")\n if len(parts) < 4 or parts[0] != \"credential_ref\" or parts[1] != \"agent\":\n return None\n agent_id = parts[2].strip()\n secret_name = parts[3].strip() or \"api_key\"\n if not agent_id:\n return None\n from services.api.app.shared.database import connect\n\n with connect() as connection:\n row = connection.execute(\n \"\"\"\n SELECT secret_value\n FROM agent_credentials\n WHERE agent_id = ? AND secret_name = ?\n \"\"\",\n (agent_id, secret_name),\n ).fetchone()\n if row is None:\n return None\n value = row[\"secret_value\"]\n return value if isinstance(value, str) and value.strip() else None\n\n\ndef _validate_adapter_backend_pair(adapter_kind: str, backend_type: str) -> None:\n if adapter_kind == \"custom_openai\" and backend_type != \"model_agent_backend\":\n raise ValidationError(\n \"custom_openai requires backend_type=model_agent_backend.\",\n code=\"provider_config_adapter_backend_mismatch\",\n )\n if adapter_kind in {\"codex_cli\", \"claude_code_cli\", \"opencode_http\"} and backend_type != \"coding_agent_backend\":\n raise ValidationError(\n f\"{adapter_kind} requires backend_type=coding_agent_backend.\",\n code=\"provider_config_adapter_backend_mismatch\",\n )\n if adapter_kind == \"disabled\" and backend_type == \"tool_integration\":\n raise ValidationError(\n \"disabled Agent Adapters cannot use backend_type=tool_integration.\",\n code=\"provider_config_adapter_backend_mismatch\",\n )\n\n\ndef _backend_type_for_adapter(adapter_kind: str) -> str:\n if adapter_kind == \"custom_openai\":\n return \"model_agent_backend\"\n if adapter_kind in {\"codex_cli\", \"claude_code_cli\", \"opencode_http\"}:\n return \"coding_agent_backend\"\n return \"model_agent_backend\"\n\n\ndef _credential_source(value: object) -> str | dict[str, str] | None:\n if value is None:\n return None\n if isinstance(value, str):\n clean = value.strip()\n if _ENV_NAME_RE.match(clean) or _CREDENTIAL_REF_RE.match(clean):\n return clean\n raise ValidationError(\n \"credential_source must be an environment variable name or CredentialRef.\",\n code=\"provider_config_invalid_credential_source\",\n )\n if isinstance(value, dict):\n forbidden = {\"value\", \"secret\", \"api_key\", \"token\", \"password\"}\n if any(key in value for key in forbidden):\n raise ValidationError(\n \"credential_source must not contain raw secret values.\",\n code=\"provider_config_secret_value_forbidden\",\n )\n if value.get(\"type\") != \"credential_ref\" or not isinstance(value.get(\"ref\"), str) or not value[\"ref\"].strip():\n raise ValidationError(\n \"CredentialRef must have type=credential_ref and a non-empty ref.\",\n code=\"provider_config_invalid_credential_source\",\n )\n return {\"type\": \"credential_ref\", \"ref\": value[\"ref\"].strip()}\n raise ValidationError(\n \"credential_source must be an environment variable name or CredentialRef.\",\n code=\"provider_config_invalid_credential_source\",\n )\n\n\ndef _reject_secret_like_config(config: ProviderConfig) -> None:\n for field in (\"api_base\", \"model\", \"executable_path\"):\n value = getattr(config, field)\n if isinstance(value, str) and _looks_like_secret(value):\n raise ValidationError(\n f\"{field} must not contain raw secret values.\",\n code=\"provider_config_secret_value_forbidden\",\n )\n\n\ndef _looks_like_secret(value: str) -> bool:\n clean = value.strip()\n secret_prefixes = (\"sk-\", \"sk_\", \"sk-proj-\", \"ghp_\", \"xoxb-\", \"ya29.\", \"Bearer \")\n return clean.startswith(secret_prefixes)\n\n\ndef _enum(raw: dict[str, Any], field: str, allowed: set[str]) -> str:\n value = _required_clean_string(raw.get(field), field)\n if value not in allowed:\n raise ValidationError(f\"Unsupported {field}: {value}\", code=f\"provider_config_invalid_{field}\")\n return value\n\n\ndef _required_clean_string(value: object, field: str) -> str:\n if not isinstance(value, str) or not value.strip():\n raise ValidationError(f\"{field} must be a non-empty string.\", code=\"provider_config_invalid\")\n return value.strip()\n\n\ndef _optional_clean_string(value: object, field: str) -> str | None:\n if value is None:\n return None\n return _required_clean_string(value, field)\n\n\ndef _positive_int(value: object, field: str, *, default: int) -> int:\n if value is None:\n return default\n if isinstance(value, bool):\n raise ValidationError(f\"{field} must be a positive integer.\", code=\"provider_config_invalid\")\n try:\n parsed = int(value)\n except (TypeError, ValueError) as exc:\n raise ValidationError(f\"{field} must be a positive integer.\", code=\"provider_config_invalid\") from exc\n if parsed <= 0:\n raise ValidationError(f\"{field} must be a positive integer.\", code=\"provider_config_invalid\")\n return parsed\n\n\ndef _temperature(value: object) -> float:\n if value is None:\n return 0.2\n if isinstance(value, bool):\n raise ValidationError(\"temperature must be a number.\", code=\"provider_config_invalid\")\n try:\n parsed = float(value)\n except (TypeError, ValueError) as exc:\n raise ValidationError(\"temperature must be a number.\", code=\"provider_config_invalid\") from exc\n if parsed < 0:\n raise ValidationError(\"temperature must be non-negative.\", code=\"provider_config_invalid\")\n return parsed\n\n\ndef _string_list(value: object, field: str) -> list[str]:\n if value is None:\n return []\n if not isinstance(value, list):\n raise ValidationError(f\"{field} must be a list.\", code=\"provider_config_invalid\")\n result: list[str] = []\n for item in value:\n if not isinstance(item, str) or not item.strip():\n raise ValidationError(f\"{field} must contain non-empty strings.\", code=\"provider_config_invalid\")\n result.append(item.strip())\n return result\n\n\ndef _provider(agent_profile: dict[str, object]) -> str | None:\n value = agent_profile.get(\"provider\")\n return value.strip().lower() if isinstance(value, str) and value.strip() else None\n\n\ndef _profile_string(agent_profile: dict[str, object], field: str) -> str | None:\n value = agent_profile.get(field)\n return value.strip() if isinstance(value, str) and value.strip() else None\n\n\ndef _profile_int(agent_profile: dict[str, object], field: str) -> int | None:\n value = agent_profile.get(field)\n if isinstance(value, bool) or value is None:\n return None\n try:\n parsed = int(value)\n except (TypeError, ValueError):\n return None\n return parsed if parsed > 0 else None\n\n\ndef _profile_float(agent_profile: dict[str, object], field: str) -> float | None:\n value = agent_profile.get(field)\n if isinstance(value, bool) or value is None:\n return None\n try:\n parsed = float(value)\n except (TypeError, ValueError):\n return None\n return parsed if parsed >= 0 else None\n\n\ndef _profile_string_list(agent_profile: dict[str, object], field: str) -> list[str]:\n value = agent_profile.get(field)\n if not isinstance(value, list):\n return []\n return [item.strip() for item in value if isinstance(item, str) and item.strip()]\n\n\ndef _profile_credential_source(agent_profile: dict[str, object]) -> object | None:\n value = agent_profile.get(\"credential_source\")\n if isinstance(value, (str, dict)):\n return value\n return None\n\n\ndef _first_env(names: object) -> str | None:\n if not isinstance(names, list):\n return None\n for name in names:\n if not isinstance(name, str):\n continue\n value = _env_value(name)\n if value:\n return value\n return None\n\n\ndef _first_env_name(names: object) -> str | None:\n if not isinstance(names, list):\n return None\n for name in names:\n if not isinstance(name, str):\n continue\n if _env_value(name):\n return name\n return None\n\n\ndef _first_executable_env(names: object) -> str | None:\n if not isinstance(names, list):\n return None\n first_configured: str | None = None\n for name in names:\n if not isinstance(name, str):\n continue\n value = _env_value(name)\n if not value:\n continue\n if first_configured is None:\n first_configured = value\n if _executable_candidate_exists(value):\n return value\n return first_configured\n\n\ndef _first_env_int(names: object) -> int | None:\n value = _first_env(names)\n if value is None:\n return None\n try:\n parsed = int(value)\n except ValueError:\n return None\n return parsed if parsed > 0 else None\n\n\ndef _first_env_float(names: object) -> float | None:\n value = _first_env(names)\n if value is None:\n return None\n try:\n parsed = float(value)\n except ValueError:\n return None\n return parsed if parsed >= 0 else None\n\n\ndef _env_key(provider: str | None) -> str:\n if not provider:\n return \"UNKNOWN\"\n return re.sub(r\"[^A-Z0-9]+\", \"_\", provider.upper()).strip(\"_\") or \"UNKNOWN\"\n\n\ndef _env_value(name: str) -> str | None:\n value = get_settings().env_value(name)\n if value is not None:\n return value\n return os.getenv(name)\n\n\ndef _executable_candidate_exists(value: str) -> bool:\n clean = value.strip().strip('\"').strip(\"'\")\n if any(separator in clean for separator in (\"/\", \"\\\\\")):\n return Path(clean).expanduser().exists()\n return shutil.which(clean) is not None\n"}, "7dae2bf8c81471f46ad055fb679046d3f2b2fa69": {"t": 1785768583.3794575, "n": 25267, "h": "87bef06ab0793168", "c": "from __future__ import annotations\n\nimport os\nimport re\nimport shutil\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Any\n\nfrom services.api.app.shared.errors import ValidationError\nfrom services.api.app.shared.settings import get_settings\n\n\nBACKEND_TYPES = {\"model_agent_backend\", \"coding_agent_backend\", \"tool_integration\"}\nADAPTER_KINDS = {\"custom_openai\", \"codex_cli\", \"claude_code_cli\", \"opencode_http\", \"disabled\"}\nWORKSPACE_MODES = {\"none\", \"read_only\", \"readonly_chat\", \"workspace_write\"}\nHEALTH_CHECK_STRATEGIES = {\"direct_probe\", \"metadata_probe\", \"none\"}\n\n_ENV_NAME_RE = re.compile(r\"^[A-Z_][A-Z0-9_]*$\")\n_CREDENTIAL_REF_RE = re.compile(r\"^credential_ref:[A-Za-z0-9_.:/-]+$\")\nCUSTOM_OPENAI_PROVIDER_KEYS = (\n \"qwen_turbo\",\n \"volc_deepseek_flash\",\n \"volc_deepseek_pro\",\n \"deepseek_official\",\n)\n\n_PROVIDER_DEFAULTS: dict[str, dict[str, object]] = {\n \"custom_openai\": {\n \"adapter_kind\": \"custom_openai\",\n \"credential_env\": [\"AGENTHUB_CUSTOM_OPENAI_API_KEY\", \"CUSTOM_OPENAI_API_KEY\"],\n \"credential_source\": \"AGENTHUB_CUSTOM_OPENAI_API_KEY\",\n },\n \"openai\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\"AGENTHUB_OPENAI_API_BASE\", \"OPENAI_API_BASE\", \"OPENAI_BASE_URL\"],\n \"model_env\": [\"AGENTHUB_OPENAI_MODEL\", \"OPENAI_MODEL\"],\n \"credential_source\": \"OPENAI_API_KEY\",\n },\n \"deepseek\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\n \"AGENTHUB_PROVIDER_DEEPSEEK_API_BASE\",\n \"AGENTHUB_DEEPSEEK_API_BASE\",\n \"DEEPSEEK_API_BASE\",\n \"AGENTHUB_CUSTOM_OPENAI_API_BASE\",\n ],\n \"model_env\": [\n \"AGENTHUB_PROVIDER_DEEPSEEK_MODEL\",\n \"AGENTHUB_DEEPSEEK_MODEL\",\n \"DEEPSEEK_MODEL\",\n \"AGENTHUB_CUSTOM_OPENAI_MODEL\",\n ],\n \"credential_env\": [\n \"AGENTHUB_PROVIDER_DEEPSEEK_API_KEY\",\n \"AGENTHUB_DEEPSEEK_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"AGENTHUB_CUSTOM_OPENAI_API_KEY\",\n ],\n \"credential_source\": \"DEEPSEEK_API_KEY\",\n },\n \"zhipu\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\"AGENTHUB_ZHIPU_API_BASE\", \"ZHIPU_API_BASE\"],\n \"model_env\": [\"AGENTHUB_ZHIPU_MODEL\", \"ZHIPU_MODEL\"],\n \"credential_source\": \"ZHIPU_API_KEY\",\n },\n \"doubao\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\"AGENTHUB_DOUBAO_API_BASE\", \"DOUBAO_API_BASE\"],\n \"model_env\": [\"AGENTHUB_DOUBAO_MODEL\", \"DOUBAO_MODEL\"],\n \"credential_source\": \"DOUBAO_API_KEY\",\n },\n \"qwen\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\n \"AGENTHUB_PROVIDER_QWEN_API_BASE\",\n \"AGENTHUB_QWEN_TURBO_API_BASE\",\n \"AGENTHUB_QWEN_API_BASE\",\n \"AGENTHUB_CUSTOM_OPENAI_API_BASE\",\n ],\n \"model_env\": [\n \"AGENTHUB_PROVIDER_QWEN_MODEL\",\n \"AGENTHUB_QWEN_TURBO_MODEL\",\n \"AGENTHUB_QWEN_MODEL\",\n \"AGENTHUB_CUSTOM_OPENAI_MODEL\",\n ],\n \"credential_env\": [\n \"AGENTHUB_PROVIDER_QWEN_API_KEY\",\n \"AGENTHUB_QWEN_TURBO_API_KEY\",\n \"AGENTHUB_QWEN_API_KEY\",\n \"AGENTHUB_CUSTOM_OPENAI_API_KEY\",\n ],\n \"credential_source\": \"QWEN_API_KEY\",\n },\n \"qwen_turbo\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\n \"AGENTHUB_PROVIDER_QWEN_API_BASE\",\n \"AGENTHUB_QWEN_TURBO_API_BASE\",\n \"AGENTHUB_QWEN_API_BASE\",\n \"AGENTHUB_CUSTOM_OPENAI_API_BASE\",\n ],\n \"model_env\": [\n \"AGENTHUB_PROVIDER_QWEN_MODEL\",\n \"AGENTHUB_QWEN_TURBO_MODEL\",\n \"AGENTHUB_QWEN_MODEL\",\n \"AGENTHUB_CUSTOM_OPENAI_MODEL\",\n ],\n \"credential_env\": [\n \"AGENTHUB_PROVIDER_QWEN_API_KEY\",\n \"AGENTHUB_QWEN_TURBO_API_KEY\",\n \"AGENTHUB_QWEN_API_KEY\",\n \"AGENTHUB_CUSTOM_OPENAI_API_KEY\",\n ],\n \"credential_source\": \"AGENTHUB_PROVIDER_QWEN_API_KEY\",\n \"strict_env\": True,\n },\n \"volc_deepseek_flash\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\"AGENTHUB_PROVIDER_VOLC_DEEPSEEK_API_BASE\"],\n \"model_env\": [\"AGENTHUB_PROVIDER_VOLC_DEEPSEEK_FLASH_MODEL\"],\n \"credential_source\": \"AGENTHUB_PROVIDER_VOLC_DEEPSEEK_API_KEY\",\n \"strict_env\": True,\n },\n \"volc_deepseek_pro\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\"AGENTHUB_PROVIDER_VOLC_DEEPSEEK_API_BASE\"],\n \"model_env\": [\"AGENTHUB_PROVIDER_VOLC_DEEPSEEK_PRO_MODEL\"],\n \"credential_source\": \"AGENTHUB_PROVIDER_VOLC_DEEPSEEK_API_KEY\",\n \"strict_env\": True,\n },\n \"deepseek_official\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\n \"AGENTHUB_PROVIDER_DEEPSEEK_API_BASE\",\n \"AGENTHUB_DEEPSEEK_API_BASE\",\n \"DEEPSEEK_API_BASE\",\n \"AGENTHUB_CUSTOM_OPENAI_API_BASE\",\n ],\n \"model_env\": [\n \"AGENTHUB_PROVIDER_DEEPSEEK_MODEL\",\n \"AGENTHUB_DEEPSEEK_MODEL\",\n \"DEEPSEEK_MODEL\",\n \"AGENTHUB_CUSTOM_OPENAI_MODEL\",\n ],\n \"credential_env\": [\n \"AGENTHUB_PROVIDER_DEEPSEEK_API_KEY\",\n \"AGENTHUB_DEEPSEEK_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"AGENTHUB_CUSTOM_OPENAI_API_KEY\",\n ],\n \"credential_source\": \"AGENTHUB_PROVIDER_DEEPSEEK_API_KEY\",\n \"strict_env\": True,\n },\n \"openrouter\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\"AGENTHUB_OPENROUTER_API_BASE\", \"OPENROUTER_API_BASE\"],\n \"model_env\": [\"AGENTHUB_OPENROUTER_MODEL\", \"OPENROUTER_MODEL\"],\n \"credential_source\": \"OPENROUTER_API_KEY\",\n },\n \"ollama\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\"AGENTHUB_OLLAMA_API_BASE\", \"OLLAMA_API_BASE\"],\n \"model_env\": [\"AGENTHUB_OLLAMA_MODEL\", \"OLLAMA_MODEL\"],\n \"credential_source\": \"OLLAMA_API_KEY\",\n },\n \"codex\": {\n \"adapter_kind\": \"codex_cli\",\n \"executable_env\": [\"AGENTHUB_CODEX_EXECUTABLE\", \"CODEX_EXECUTABLE\", \"AGENTHUB_CODEX_CLI_EXECUTABLE\"],\n \"executable_path\": \"codex\",\n },\n \"anthropic\": {\n \"adapter_kind\": \"claude_code_cli\",\n \"executable_env\": [\"AGENTHUB_CLAUDE_CODE_EXECUTABLE\", \"AGENTHUB_CLAUDE_EXECUTABLE\", \"CLAUDE_CODE_EXECUTABLE\"],\n \"executable_path\": \"claude\",\n },\n \"opencode\": {\n \"adapter_kind\": \"opencode_http\",\n \"api_base_env\": [\"OPENCODE_API_BASE\", \"AGENTHUB_OPENCODE_API_BASE\"],\n },\n # Chinese provider name aliases – map to the same env-variable chains as\n # their English counterparts so that runtime resolution works regardless of\n # which name the agent profile stores.\n \"深度求索\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\n \"AGENTHUB_PROVIDER_DEEPSEEK_API_BASE\",\n \"AGENTHUB_DEEPSEEK_API_BASE\",\n \"DEEPSEEK_API_BASE\",\n \"AGENTHUB_CUSTOM_OPENAI_API_BASE\",\n ],\n \"model_env\": [\n \"AGENTHUB_PROVIDER_DEEPSEEK_MODEL\",\n \"AGENTHUB_DEEPSEEK_MODEL\",\n \"DEEPSEEK_MODEL\",\n \"AGENTHUB_CUSTOM_OPENAI_MODEL\",\n ],\n \"credential_env\": [\n \"AGENTHUB_PROVIDER_DEEPSEEK_API_KEY\",\n \"AGENTHUB_DEEPSEEK_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"AGENTHUB_CUSTOM_OPENAI_API_KEY\",\n ],\n \"credential_source\": \"DEEPSEEK_API_KEY\",\n },\n \"阿里千问\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\n \"AGENTHUB_PROVIDER_QWEN_API_BASE\",\n \"AGENTHUB_QWEN_TURBO_API_BASE\",\n \"AGENTHUB_QWEN_API_BASE\",\n \"AGENTHUB_CUSTOM_OPENAI_API_BASE\",\n ],\n \"model_env\": [\n \"AGENTHUB_PROVIDER_QWEN_MODEL\",\n \"AGENTHUB_QWEN_TURBO_MODEL\",\n \"AGENTHUB_QWEN_MODEL\",\n \"AGENTHUB_CUSTOM_OPENAI_MODEL\",\n ],\n \"credential_env\": [\n \"AGENTHUB_PROVIDER_QWEN_API_KEY\",\n \"AGENTHUB_QWEN_TURBO_API_KEY\",\n \"AGENTHUB_QWEN_API_KEY\",\n \"AGENTHUB_CUSTOM_OPENAI_API_KEY\",\n ],\n \"credential_source\": \"QWEN_API_KEY\",\n },\n \"千问\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\n \"AGENTHUB_PROVIDER_QWEN_API_BASE\",\n \"AGENTHUB_QWEN_TURBO_API_BASE\",\n \"AGENTHUB_QWEN_API_BASE\",\n \"AGENTHUB_CUSTOM_OPENAI_API_BASE\",\n ],\n \"model_env\": [\n \"AGENTHUB_PROVIDER_QWEN_MODEL\",\n \"AGENTHUB_QWEN_TURBO_MODEL\",\n \"AGENTHUB_QWEN_MODEL\",\n \"AGENTHUB_CUSTOM_OPENAI_MODEL\",\n ],\n \"credential_env\": [\n \"AGENTHUB_PROVIDER_QWEN_API_KEY\",\n \"AGENTHUB_QWEN_TURBO_API_KEY\",\n \"AGENTHUB_QWEN_API_KEY\",\n \"AGENTHUB_CUSTOM_OPENAI_API_KEY\",\n ],\n \"credential_source\": \"QWEN_API_KEY\",\n },\n \"智谱\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\"AGENTHUB_ZHIPU_API_BASE\", \"ZHIPU_API_BASE\"],\n \"model_env\": [\"AGENTHUB_ZHIPU_MODEL\", \"ZHIPU_MODEL\"],\n \"credential_source\": \"ZHIPU_API_KEY\",\n },\n \"豆包\": {\n \"adapter_kind\": \"custom_openai\",\n \"api_base_env\": [\"AGENTHUB_DOUBAO_API_BASE\", \"DOUBAO_API_BASE\"],\n \"model_env\": [\"AGENTHUB_DOUBAO_MODEL\", \"DOUBAO_MODEL\"],\n \"credential_source\": \"DOUBAO_API_KEY\",\n },\n}\n\n\n@dataclass(frozen=True)\nclass ProviderConfig:\n provider: str | None\n adapter_kind: str\n backend_type: str\n api_base: str | None\n model: str | None\n credential_source: str | dict[str, str] | None\n executable_path: str | None\n timeout_seconds: int\n max_output_tokens: int\n temperature: float\n workspace_mode: str\n allowed_tools: list[str]\n health_check_strategy: str\n\n\ndef validate_provider_config(raw: dict[str, Any]) -> ProviderConfig:\n if not isinstance(raw, dict):\n raise ValidationError(\"ProviderConfig must be an object.\", code=\"provider_config_invalid\")\n\n provider = _optional_clean_string(raw.get(\"provider\"), \"provider\")\n adapter_kind = _enum(raw, \"adapter_kind\", ADAPTER_KINDS)\n backend_type = _enum(raw, \"backend_type\", BACKEND_TYPES)\n _validate_adapter_backend_pair(adapter_kind, backend_type)\n\n config = ProviderConfig(\n provider=provider,\n adapter_kind=adapter_kind,\n backend_type=backend_type,\n api_base=_optional_clean_string(raw.get(\"api_base\"), \"api_base\"),\n model=_optional_clean_string(raw.get(\"model\"), \"model\"),\n credential_source=_credential_source(raw.get(\"credential_source\")),\n executable_path=_optional_clean_string(raw.get(\"executable_path\"), \"executable_path\"),\n timeout_seconds=_positive_int(raw.get(\"timeout_seconds\"), \"timeout_seconds\", default=30),\n max_output_tokens=_positive_int(raw.get(\"max_output_tokens\"), \"max_output_tokens\", default=4096),\n temperature=_temperature(raw.get(\"temperature\")),\n workspace_mode=_enum(raw, \"workspace_mode\", WORKSPACE_MODES),\n allowed_tools=_string_list(raw.get(\"allowed_tools\"), \"allowed_tools\"),\n health_check_strategy=_enum(raw, \"health_check_strategy\", HEALTH_CHECK_STRATEGIES),\n )\n _reject_secret_like_config(config)\n return config\n\n\ndef provider_config_from_agent(agent_profile: dict[str, object]) -> ProviderConfig:\n provider = _provider(agent_profile)\n defaults = _PROVIDER_DEFAULTS.get(provider or \"\", {})\n adapter_kind = _profile_string(agent_profile, \"adapter_kind\") or str(defaults.get(\"adapter_kind\") or \"disabled\")\n allow_generic_env = defaults.get(\"strict_env\") is not True\n\n raw = {\n \"provider\": provider,\n \"adapter_kind\": adapter_kind,\n \"backend_type\": _backend_type_for_adapter(adapter_kind),\n \"api_base\": _profile_string(agent_profile, \"api_base\")\n or _first_env(defaults.get(\"api_base_env\"))\n or _first_env([f\"AGENTHUB_{_env_key(provider)}_API_BASE\"] if provider and allow_generic_env else None),\n \"model\": _profile_string(agent_profile, \"model\")\n or _first_env(defaults.get(\"model_env\"))\n or _first_env([f\"AGENTHUB_{_env_key(provider)}_MODEL\"] if provider and allow_generic_env else None),\n \"credential_source\": _profile_credential_source(agent_profile)\n or _first_env([f\"AGENTHUB_{_env_key(provider)}_CREDENTIAL_SOURCE\"] if provider and allow_generic_env else None)\n or _first_env_name(defaults.get(\"credential_env\"))\n or defaults.get(\"credential_source\"),\n \"executable_path\": _profile_string(agent_profile, \"executable_path\")\n or _first_executable_env(defaults.get(\"executable_env\"))\n or defaults.get(\"executable_path\"),\n \"timeout_seconds\": _profile_int(agent_profile, \"timeout_seconds\")\n or _first_env_int([f\"AGENTHUB_{_env_key(provider)}_TIMEOUT_SECONDS\"] if provider else None)\n or 30,\n \"max_output_tokens\": _profile_int(agent_profile, \"max_output_tokens\")\n or _first_env_int([f\"AGENTHUB_{_env_key(provider)}_MAX_OUTPUT_TOKENS\"] if provider else None)\n or 4096,\n \"temperature\": _profile_float(agent_profile, \"temperature\")\n or _first_env_float([f\"AGENTHUB_{_env_key(provider)}_TEMPERATURE\"] if provider else None)\n or 0.2,\n \"workspace_mode\": _profile_string(agent_profile, \"workspace_mode\")\n or (\n \"read_only\"\n if adapter_kind in {\"codex_cli\", \"claude_code_cli\"}\n else \"readonly_chat\"\n if adapter_kind == \"custom_openai\"\n else \"none\"\n ),\n \"allowed_tools\": _profile_string_list(agent_profile, \"allowed_tools\"),\n \"health_check_strategy\": _profile_string(agent_profile, \"health_check_strategy\") or \"direct_probe\",\n }\n return validate_provider_config(raw)\n\n\ndef custom_openai_provider_config(provider_key: str) -> ProviderConfig:\n clean = provider_key.strip().lower() if isinstance(provider_key, str) else \"\"\n if clean not in CUSTOM_OPENAI_PROVIDER_KEYS:\n raise ValidationError(\n f\"Unsupported A7-3.1 custom_openai provider: {provider_key}\",\n code=\"provider_config_invalid_provider\",\n )\n return provider_config_from_agent(\n {\n \"provider\": clean,\n \"adapter_kind\": \"custom_openai\",\n \"execution_enabled\": True,\n \"configured\": True,\n \"health_status\": \"configured\",\n }\n )\n\n\ndef custom_openai_provider_configs_from_environment() -> dict[str, ProviderConfig]:\n return {provider_key: custom_openai_provider_config(provider_key) for provider_key in CUSTOM_OPENAI_PROVIDER_KEYS}\n\n\ndef credential_env_name(config: ProviderConfig) -> str | None:\n if isinstance(config.credential_source, str) and _ENV_NAME_RE.match(config.credential_source):\n return config.credential_source\n return None\n\n\ndef credential_value_from_environment(config: ProviderConfig) -> str | None:\n if isinstance(config.credential_source, str) and config.credential_source.startswith(\"credential_ref:agent:\"):\n return _credential_value_from_agent_ref(config.credential_source)\n env_name = credential_env_name(config)\n if env_name is None:\n return None\n value = _env_value(env_name)\n return value if isinstance(value, str) and value.strip() else None\n\n\ndef _credential_value_from_agent_ref(source: str) -> str | None:\n parts = source.split(\":\")\n if len(parts) < 4 or parts[0] != \"credential_ref\" or parts[1] != \"agent\":\n return None\n agent_id = parts[2].strip()\n secret_name = parts[3].strip() or \"api_key\"\n if not agent_id:\n return None\n from services.api.app.shared.database import connect\n\n with connect() as connection:\n row = connection.execute(\n \"\"\"\n SELECT secret_value\n FROM agent_credentials\n WHERE agent_id = ? AND secret_name = ?\n \"\"\",\n (agent_id, secret_name),\n ).fetchone()\n if row is None:\n return None\n value = row[\"secret_value\"]\n return value if isinstance(value, str) and value.strip() else None\n\n\ndef _validate_adapter_backend_pair(adapter_kind: str, backend_type: str) -> None:\n if adapter_kind == \"custom_openai\" and backend_type != \"model_agent_backend\":\n raise ValidationError(\n \"custom_openai requires backend_type=model_agent_backend.\",\n code=\"provider_config_adapter_backend_mismatch\",\n )\n if adapter_kind in {\"codex_cli\", \"claude_code_cli\", \"opencode_http\"} and backend_type != \"coding_agent_backend\":\n raise ValidationError(\n f\"{adapter_kind} requires backend_type=coding_agent_backend.\",\n code=\"provider_config_adapter_backend_mismatch\",\n )\n if adapter_kind == \"disabled\" and backend_type == \"tool_integration\":\n raise ValidationError(\n \"disabled Agent Adapters cannot use backend_type=tool_integration.\",\n code=\"provider_config_adapter_backend_mismatch\",\n )\n\n\ndef _backend_type_for_adapter(adapter_kind: str) -> str:\n if adapter_kind == \"custom_openai\":\n return \"model_agent_backend\"\n if adapter_kind in {\"codex_cli\", \"claude_code_cli\", \"opencode_http\"}:\n return \"coding_agent_backend\"\n return \"model_agent_backend\"\n\n\ndef _credential_source(value: object) -> str | dict[str, str] | None:\n if value is None:\n return None\n if isinstance(value, str):\n clean = value.strip()\n if _ENV_NAME_RE.match(clean) or _CREDENTIAL_REF_RE.match(clean):\n return clean\n raise ValidationError(\n \"credential_source must be an environment variable name or CredentialRef.\",\n code=\"provider_config_invalid_credential_source\",\n )\n if isinstance(value, dict):\n forbidden = {\"value\", \"secret\", \"api_key\", \"token\", \"password\"}\n if any(key in value for key in forbidden):\n raise ValidationError(\n \"credential_source must not contain raw secret values.\",\n code=\"provider_config_secret_value_forbidden\",\n )\n if value.get(\"type\") != \"credential_ref\" or not isinstance(value.get(\"ref\"), str) or not value[\"ref\"].strip():\n raise ValidationError(\n \"CredentialRef must have type=credential_ref and a non-empty ref.\",\n code=\"provider_config_invalid_credential_source\",\n )\n return {\"type\": \"credential_ref\", \"ref\": value[\"ref\"].strip()}\n raise ValidationError(\n \"credential_source must be an environment variable name or CredentialRef.\",\n code=\"provider_config_invalid_credential_source\",\n )\n\n\ndef _reject_secret_like_config(config: ProviderConfig) -> None:\n for field in (\"api_base\", \"model\", \"executable_path\"):\n value = getattr(config, field)\n if isinstance(value, str) and _looks_like_secret(value):\n raise ValidationError(\n f\"{field} must not contain raw secret values.\",\n code=\"provider_config_secret_value_forbidden\",\n )\n\n\ndef _looks_like_secret(value: str) -> bool:\n clean = value.strip()\n secret_prefixes = (\"sk-\", \"sk_\", \"sk-proj-\", \"ghp_\", \"xoxb-\", \"ya29.\", \"Bearer \")\n return clean.startswith(secret_prefixes)\n\n\ndef _enum(raw: dict[str, Any], field: str, allowed: set[str]) -> str:\n value = _required_clean_string(raw.get(field), field)\n if value not in allowed:\n raise ValidationError(f\"Unsupported {field}: {value}\", code=f\"provider_config_invalid_{field}\")\n return value\n\n\ndef _required_clean_string(value: object, field: str) -> str:\n if not isinstance(value, str) or not value.strip():\n raise ValidationError(f\"{field} must be a non-empty string.\", code=\"provider_config_invalid\")\n return value.strip()\n\n\ndef _optional_clean_string(value: object, field: str) -> str | None:\n if value is None:\n return None\n return _required_clean_string(value, field)\n\n\ndef _positive_int(value: object, field: str, *, default: int) -> int:\n if value is None:\n return default\n if isinstance(value, bool):\n raise ValidationError(f\"{field} must be a positive integer.\", code=\"provider_config_invalid\")\n try:\n parsed = int(value)\n except (TypeError, ValueError) as exc:\n raise ValidationError(f\"{field} must be a positive integer.\", code=\"provider_config_invalid\") from exc\n if parsed <= 0:\n raise ValidationError(f\"{field} must be a positive integer.\", code=\"provider_config_invalid\")\n return parsed\n\n\ndef _temperature(value: object) -> float:\n if value is None:\n return 0.2\n if isinstance(value, bool):\n raise ValidationError(\"temperature must be a number.\", code=\"provider_config_invalid\")\n try:\n parsed = float(value)\n except (TypeError, ValueError) as exc:\n raise ValidationError(\"temperature must be a number.\", code=\"provider_config_invalid\") from exc\n if parsed < 0:\n raise ValidationError(\"temperature must be non-negative.\", code=\"provider_config_invalid\")\n return parsed\n\n\ndef _string_list(value: object, field: str) -> list[str]:\n if value is None:\n return []\n if not isinstance(value, list):\n raise ValidationError(f\"{field} must be a list.\", code=\"provider_config_invalid\")\n result: list[str] = []\n for item in value:\n if not isinstance(item, str) or not item.strip():\n raise ValidationError(f\"{field} must contain non-empty strings.\", code=\"provider_config_invalid\")\n result.append(item.strip())\n return result\n\n\ndef _provider(agent_profile: dict[str, object]) -> str | None:\n value = agent_profile.get(\"provider\")\n return value.strip().lower() if isinstance(value, str) and value.strip() else None\n\n\ndef _profile_string(agent_profile: dict[str, object], field: str) -> str | None:\n value = agent_profile.get(field)\n return value.strip() if isinstance(value, str) and value.strip() else None\n\n\ndef _profile_int(agent_profile: dict[str, object], field: str) -> int | None:\n value = agent_profile.get(field)\n if isinstance(value, bool) or value is None:\n return None\n try:\n parsed = int(value)\n except (TypeError, ValueError):\n return None\n return parsed if parsed > 0 else None\n\n\ndef _profile_float(agent_profile: dict[str, object], field: str) -> float | None:\n value = agent_profile.get(field)\n if isinstance(value, bool) or value is None:\n return None\n try:\n parsed = float(value)\n except (TypeError, ValueError):\n return None\n return parsed if parsed >= 0 else None\n\n\ndef _profile_string_list(agent_profile: dict[str, object], field: str) -> list[str]:\n value = agent_profile.get(field)\n if not isinstance(value, list):\n return []\n return [item.strip() for item in value if isinstance(item, str) and item.strip()]\n\n\ndef _profile_credential_source(agent_profile: dict[str, object]) -> object | None:\n value = agent_profile.get(\"credential_source\")\n if isinstance(value, (str, dict)):\n return value\n return None\n\n\ndef _first_env(names: object) -> str | None:\n if not isinstance(names, list):\n return None\n for name in names:\n if not isinstance(name, str):\n continue\n value = _env_value(name)\n if value:\n return value\n return None\n\n\ndef _first_env_name(names: object) -> str | None:\n if not isinstance(names, list):\n return None\n for name in names:\n if not isinstance(name, str):\n continue\n if _env_value(name):\n return name\n return None\n\n\ndef _first_executable_env(names: object) -> str | None:\n if not isinstance(names, list):\n return None\n first_configured: str | None = None\n for name in names:\n if not isinstance(name, str):\n continue\n value = _env_value(name)\n if not value:\n continue\n if first_configured is None:\n first_configured = value\n if _executable_candidate_exists(value):\n return value\n return first_configured\n\n\ndef _first_env_int(names: object) -> int | None:\n value = _first_env(names)\n if value is None:\n return None\n try:\n parsed = int(value)\n except ValueError:\n return None\n return parsed if parsed > 0 else None\n\n\ndef _first_env_float(names: object) -> float | None:\n value = _first_env(names)\n if value is None:\n return None\n try:\n parsed = float(value)\n except ValueError:\n return None\n return parsed if parsed >= 0 else None\n\n\ndef _env_key(provider: str | None) -> str:\n if not provider:\n return \"UNKNOWN\"\n return re.sub(r\"[^A-Z0-9]+\", \"_\", provider.upper()).strip(\"_\") or \"UNKNOWN\"\n\n\ndef _env_value(name: str) -> str | None:\n value = get_settings().env_value(name)\n if value is not None:\n return value\n return os.getenv(name)\n\n\ndef _executable_candidate_exists(value: str) -> bool:\n clean = value.strip().strip('\"').strip(\"'\")\n if any(separator in clean for separator in (\"/\", \"\\\\\")):\n return Path(clean).expanduser().exists()\n return shutil.which(clean) is not None\n"}}}}