Files

1 line
60 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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<NamedError[\"toObject\"]>\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<void>\n}) {\n return Schedule.fromStepWithMetadata(\n Effect.succeed((meta: Schedule.InputMetadata<unknown>) => {\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"}}}}