multiagentos 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. agents/__init__.py +1 -0
  2. agents/catalog.py +52 -0
  3. agents/registry.py +11 -0
  4. core/__init__.py +38 -0
  5. core/artifacts.py +47 -0
  6. core/chat_agent_bridge.py +280 -0
  7. core/chat_agent_registry.py +61 -0
  8. core/chat_agent_router.py +85 -0
  9. core/chat_session.py +62 -0
  10. core/contracts/__init__.py +22 -0
  11. core/contracts/agent.py +21 -0
  12. core/contracts/ai.py +25 -0
  13. core/contracts/capability.py +44 -0
  14. core/contracts/chat_agent.py +34 -0
  15. core/contracts/checkpoint.py +78 -0
  16. core/contracts/discovery.py +30 -0
  17. core/contracts/execution.py +28 -0
  18. core/contracts/github.py +74 -0
  19. core/contracts/handoff.py +56 -0
  20. core/contracts/health.py +206 -0
  21. core/contracts/human_review.py +20 -0
  22. core/contracts/ide.py +113 -0
  23. core/contracts/mcp.py +76 -0
  24. core/contracts/model_runtime.py +25 -0
  25. core/contracts/planning.py +25 -0
  26. core/contracts/profile.py +66 -0
  27. core/contracts/quota.py +62 -0
  28. core/contracts/repository.py +34 -0
  29. core/contracts/resume.py +55 -0
  30. core/contracts/runtime.py +19 -0
  31. core/contracts/vyrelon_runtime.py +118 -0
  32. core/contracts/work_unit.py +48 -0
  33. core/delegation.py +33 -0
  34. core/handoff.py +68 -0
  35. core/lifecycle.py +85 -0
  36. core/multi_agent_workflow.py +574 -0
  37. core/orchestrator.py +51 -0
  38. core/planning.py +22 -0
  39. core/registry.py +44 -0
  40. core/routing.py +195 -0
  41. core/state.py +155 -0
  42. installer/__init__.py +1 -0
  43. installer/init.py +71 -0
  44. integrations/__init__.py +0 -0
  45. integrations/github/__init__.py +3 -0
  46. integrations/github/gateway.py +164 -0
  47. integrations/openai/__init__.py +1 -0
  48. integrations/openai/chat_agent.py +147 -0
  49. multiagentos/__init__.py +1 -0
  50. multiagentos/cli.py +759 -0
  51. multiagentos-0.4.0.dist-info/METADATA +7 -0
  52. multiagentos-0.4.0.dist-info/RECORD +122 -0
  53. multiagentos-0.4.0.dist-info/WHEEL +5 -0
  54. multiagentos-0.4.0.dist-info/entry_points.txt +2 -0
  55. multiagentos-0.4.0.dist-info/top_level.txt +7 -0
  56. profiles/__init__.py +1 -0
  57. profiles/common.py +11 -0
  58. profiles/detector.py +67 -0
  59. profiles/registry.py +46 -0
  60. profiles/resolver.py +63 -0
  61. runtime/__init__.py +5 -0
  62. runtime/agent/__init__.py +1 -0
  63. runtime/agent/ide.py +74 -0
  64. runtime/agent/model.py +197 -0
  65. runtime/agent/model_backed.py +49 -0
  66. runtime/agent/process.py +21 -0
  67. runtime/builtin_tools.py +57 -0
  68. runtime/capability.py +143 -0
  69. runtime/chat_adapter_registry.py +43 -0
  70. runtime/chat_config.py +57 -0
  71. runtime/chat_execution.py +52 -0
  72. runtime/config/__init__.py +1 -0
  73. runtime/config/loader.py +67 -0
  74. runtime/discovery.py +334 -0
  75. runtime/execution_config.py +71 -0
  76. runtime/execution_registry.py +50 -0
  77. runtime/git.py +84 -0
  78. runtime/github.py +56 -0
  79. runtime/github_probe.py +23 -0
  80. runtime/health.py +14 -0
  81. runtime/ide/__init__.py +6 -0
  82. runtime/ide/bridge.py +198 -0
  83. runtime/ide/registry.py +24 -0
  84. runtime/ide/runtime.py +35 -0
  85. runtime/local/__init__.py +18 -0
  86. runtime/local/filesystem.py +58 -0
  87. runtime/local/patch.py +79 -0
  88. runtime/local/path_security.py +31 -0
  89. runtime/local/permissions.py +20 -0
  90. runtime/local/process.py +56 -0
  91. runtime/local/shell.py +97 -0
  92. runtime/mcp/__init__.py +7 -0
  93. runtime/mcp/client.py +120 -0
  94. runtime/mcp/config.py +36 -0
  95. runtime/mcp/policy.py +57 -0
  96. runtime/mcp/proxy.py +30 -0
  97. runtime/mcp/server.py +137 -0
  98. runtime/mcp/session.py +19 -0
  99. runtime/model/__init__.py +5 -0
  100. runtime/model/ai_runtime.py +165 -0
  101. runtime/model/cli.py +49 -0
  102. runtime/model/config.py +98 -0
  103. runtime/model/credentials.py +42 -0
  104. runtime/model/factory.py +82 -0
  105. runtime/model/http.py +78 -0
  106. runtime/model/invoker.py +16 -0
  107. runtime/model/materializer.py +101 -0
  108. runtime/model/native.py +244 -0
  109. runtime/model/providers.py +55 -0
  110. runtime/model/registry.py +26 -0
  111. runtime/model/runtime_loader.py +51 -0
  112. runtime/model_control.py +136 -0
  113. runtime/multi_agent.py +142 -0
  114. runtime/policy.py +28 -0
  115. runtime/process.py +41 -0
  116. runtime/quota.py +259 -0
  117. runtime/repository.py +103 -0
  118. runtime/repository_tools.py +62 -0
  119. runtime/status.py +87 -0
  120. runtime/tool_calling.py +134 -0
  121. runtime/validation.py +121 -0
  122. runtime/vyrelon.py +1557 -0
@@ -0,0 +1,98 @@
1
+ """Load provider/model configuration from project JSON without vendor-specific branches."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from core.contracts.ai import AIProvider, ModelSpec
10
+ from runtime.model.providers import AIProviderRegistry
11
+
12
+
13
+ DEFAULT_CONFIG_PATH = ".multiagentos/providers.json"
14
+
15
+
16
+ class ProviderConfigLoader:
17
+ """Parse a declarative provider configuration into the VYRELON registry."""
18
+
19
+ def load(
20
+ self,
21
+ path: Path,
22
+ registry: AIProviderRegistry | None = None,
23
+ ) -> AIProviderRegistry:
24
+ target = registry or AIProviderRegistry()
25
+ payload = json.loads(path.read_text(encoding="utf-8"))
26
+ if not isinstance(payload, dict):
27
+ raise ValueError("Provider configuration root must be an object")
28
+
29
+ providers = payload.get("providers", [])
30
+ if not isinstance(providers, list):
31
+ raise ValueError("'providers' must be a list")
32
+
33
+ for provider_data in providers:
34
+ provider = self._provider(provider_data)
35
+ if provider.id in target.list_provider_ids():
36
+ continue
37
+ target.register(provider)
38
+ return target
39
+
40
+ def _provider(self, value: Any) -> AIProvider:
41
+ if not isinstance(value, dict):
42
+ raise ValueError("Provider entry must be an object")
43
+
44
+ provider_id = self._required_string(value, "id")
45
+ kind = self._required_string(value, "kind")
46
+ models_data = value.get("models", [])
47
+ if not isinstance(models_data, list):
48
+ raise ValueError(f"Provider {provider_id}: 'models' must be a list")
49
+
50
+ models = tuple(
51
+ self._model(provider_id, item)
52
+ for item in models_data
53
+ )
54
+ metadata = value.get("metadata", {})
55
+ if not isinstance(metadata, dict):
56
+ raise ValueError(f"Provider {provider_id}: 'metadata' must be an object")
57
+
58
+ return AIProvider(
59
+ id=provider_id,
60
+ kind=kind,
61
+ models=models,
62
+ metadata=dict(metadata),
63
+ )
64
+
65
+ def _model(self, provider_id: str, value: Any) -> ModelSpec:
66
+ if not isinstance(value, dict):
67
+ raise ValueError(f"Provider {provider_id}: model entry must be an object")
68
+
69
+ model_id = self._required_string(value, "id")
70
+ capabilities = value.get("capabilities", [])
71
+ if not isinstance(capabilities, list) or not all(
72
+ isinstance(item, str) for item in capabilities
73
+ ):
74
+ raise ValueError(
75
+ f"Provider {provider_id}, model {model_id}: "
76
+ "'capabilities' must be a list of strings"
77
+ )
78
+
79
+ metadata = value.get("metadata", {})
80
+ if not isinstance(metadata, dict):
81
+ raise ValueError(
82
+ f"Provider {provider_id}, model {model_id}: "
83
+ "'metadata' must be an object"
84
+ )
85
+
86
+ return ModelSpec(
87
+ id=model_id,
88
+ provider_id=provider_id,
89
+ capabilities=frozenset(capabilities),
90
+ metadata=dict(metadata),
91
+ )
92
+
93
+ @staticmethod
94
+ def _required_string(value: dict[str, Any], key: str) -> str:
95
+ item = value.get(key)
96
+ if not isinstance(item, str) or not item:
97
+ raise ValueError(f"Configuration field '{key}' must be a non-empty string")
98
+ return item
@@ -0,0 +1,42 @@
1
+ """Environment-based credential validation for model providers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+ from typing import Mapping
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class CredentialCheck:
12
+ """Non-secret result of checking one environment-backed credential."""
13
+
14
+ environment_variable: str
15
+ present: bool
16
+
17
+
18
+ class EnvironmentCredentialResolver:
19
+ """Resolve credentials from environment variables without persisting secrets."""
20
+
21
+ def resolve(self, environment_variable: str) -> str:
22
+ value = os.environ.get(environment_variable)
23
+ if not value:
24
+ raise RuntimeError(
25
+ f"Required model credential environment variable is missing: "
26
+ f"{environment_variable}"
27
+ )
28
+ return value
29
+
30
+ def check(self, environment_variables: list[str]) -> tuple[CredentialCheck, ...]:
31
+ return tuple(
32
+ CredentialCheck(
33
+ environment_variable=name,
34
+ present=bool(os.environ.get(name)),
35
+ )
36
+ for name in environment_variables
37
+ )
38
+
39
+ def check_mapping(
40
+ self, header_env: Mapping[str, str]
41
+ ) -> tuple[CredentialCheck, ...]:
42
+ return self.check(list(header_env.values()))
@@ -0,0 +1,82 @@
1
+ """Materialize generic model adapters from declarative provider configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from runtime.model.cli import CLIModelAdapter
8
+ from runtime.model.http import HTTPModelAdapter
9
+ from runtime.policy import ExecutionPolicy
10
+
11
+
12
+ class ConfiguredAdapterFactory:
13
+ """Build only the provider-neutral adapters supported by MultiAgentOS."""
14
+
15
+ def build(
16
+ self,
17
+ adapter_id: str,
18
+ metadata: dict[str, object],
19
+ policy: ExecutionPolicy | None = None,
20
+ ):
21
+ kind = str(metadata.get("adapter_kind", adapter_id))
22
+ effective_policy = policy or ExecutionPolicy()
23
+
24
+ if kind == "cli":
25
+ command = metadata.get("command")
26
+ if not isinstance(command, list) or not all(
27
+ isinstance(part, str) and part for part in command
28
+ ):
29
+ raise ValueError(
30
+ f"CLI adapter {adapter_id} requires metadata.command as a non-empty list"
31
+ )
32
+ timeout = metadata.get("timeout", 300)
33
+ if not isinstance(timeout, (int, float)) or timeout <= 0:
34
+ raise ValueError(f"CLI adapter {adapter_id} timeout must be positive")
35
+ return CLIModelAdapter(
36
+ command=tuple(command),
37
+ policy=effective_policy,
38
+ timeout=float(timeout),
39
+ )
40
+
41
+ if kind == "http":
42
+ endpoint = metadata.get("endpoint")
43
+ if not isinstance(endpoint, str) or not endpoint:
44
+ raise ValueError(
45
+ f"HTTP adapter {adapter_id} requires metadata.endpoint"
46
+ )
47
+
48
+ headers = self._string_mapping(metadata.get("headers", {}), "headers")
49
+ header_env = self._string_mapping(
50
+ metadata.get("header_env", {}), "header_env"
51
+ )
52
+ request_template = metadata.get("request_template", {})
53
+ if not isinstance(request_template, dict):
54
+ raise ValueError(
55
+ f"HTTP adapter {adapter_id} request_template must be an object"
56
+ )
57
+ response_path = metadata.get("response_path", ["text"])
58
+ if not isinstance(response_path, list) or not all(
59
+ isinstance(part, str) for part in response_path
60
+ ):
61
+ raise ValueError(
62
+ f"HTTP adapter {adapter_id} response_path must be a list of strings"
63
+ )
64
+ return HTTPModelAdapter(
65
+ endpoint=endpoint,
66
+ headers=headers,
67
+ header_env=header_env,
68
+ request_template=request_template,
69
+ response_path=tuple(response_path),
70
+ policy=effective_policy,
71
+ )
72
+
73
+ raise ValueError(f"Unsupported model adapter kind: {kind}")
74
+
75
+ @staticmethod
76
+ def _string_mapping(value: Any, field_name: str) -> dict[str, str]:
77
+ if not isinstance(value, dict) or not all(
78
+ isinstance(key, str) and isinstance(item, str)
79
+ for key, item in value.items()
80
+ ):
81
+ raise ValueError(f"{field_name} must be an object of strings")
82
+ return dict(value)
runtime/model/http.py ADDED
@@ -0,0 +1,78 @@
1
+ """Generic HTTP JSON model adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import urllib.request
8
+ from dataclasses import dataclass, field
9
+ from typing import Any
10
+
11
+ from core.contracts.ai import ModelSpec
12
+ from core.contracts.model_runtime import ModelRequest, ModelResponse
13
+ from runtime.policy import ExecutionPolicy
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class HTTPModelAdapter:
18
+ endpoint: str
19
+ headers: dict[str, str] = field(default_factory=dict)
20
+ header_env: dict[str, str] = field(default_factory=dict)
21
+ request_template: dict[str, Any] = field(
22
+ default_factory=lambda: {
23
+ "model": "{model_id}",
24
+ "prompt": "{prompt}",
25
+ "system": "{system}",
26
+ }
27
+ )
28
+ response_path: tuple[str, ...] = ("text",)
29
+ policy: ExecutionPolicy = ExecutionPolicy(allow_network=True)
30
+
31
+ def generate(self, model: ModelSpec, request: ModelRequest) -> ModelResponse:
32
+ if not self.policy.permits("network"):
33
+ raise PermissionError("Network model execution is disabled by policy")
34
+
35
+ payload = self._render(self.request_template, model, request)
36
+ headers = {"Content-Type": "application/json", **self.headers}
37
+ for header, env_name in self.header_env.items():
38
+ value = os.environ.get(env_name)
39
+ if not value:
40
+ raise RuntimeError(f"Required model credential environment variable is missing: {env_name}")
41
+ headers[header] = value
42
+
43
+ body = json.dumps(payload).encode("utf-8")
44
+ http_request = urllib.request.Request(
45
+ self.endpoint,
46
+ data=body,
47
+ headers=headers,
48
+ method="POST",
49
+ )
50
+ with urllib.request.urlopen(http_request, timeout=300) as response:
51
+ data = json.loads(response.read().decode("utf-8"))
52
+
53
+ value: Any = data
54
+ for part in self.response_path:
55
+ if isinstance(value, dict):
56
+ value = value[part]
57
+ elif isinstance(value, list):
58
+ value = value[int(part)]
59
+ else:
60
+ raise ValueError(f"Cannot resolve response path at: {part}")
61
+
62
+ if not isinstance(value, str):
63
+ raise ValueError("Configured model response path must resolve to text")
64
+
65
+ return ModelResponse(text=value, model_id=model.id, metadata={"adapter": "http"})
66
+
67
+ def _render(self, value: Any, model: ModelSpec, request: ModelRequest) -> Any:
68
+ if isinstance(value, str):
69
+ return (
70
+ value.replace("{model_id}", model.id)
71
+ .replace("{prompt}", request.prompt)
72
+ .replace("{system}", request.system or "")
73
+ )
74
+ if isinstance(value, dict):
75
+ return {key: self._render(item, model, request) for key, item in value.items()}
76
+ if isinstance(value, list):
77
+ return [self._render(item, model, request) for item in value]
78
+ return value
@@ -0,0 +1,16 @@
1
+ """Model invocation service."""
2
+
3
+ from core.contracts.ai import ModelSpec
4
+ from core.contracts.model_runtime import ModelRequest, ModelResponse
5
+ from runtime.model.registry import ModelAdapterRegistry
6
+
7
+
8
+ class ModelInvoker:
9
+ def __init__(self, registry: ModelAdapterRegistry) -> None:
10
+ self.registry = registry
11
+
12
+ def generate(self, model: ModelSpec, request: ModelRequest) -> ModelResponse:
13
+ adapter_id = model.metadata.get("adapter_id", model.provider_id)
14
+ if not isinstance(adapter_id, str) or not adapter_id:
15
+ raise ValueError(f"Invalid adapter id for model: {model.id}")
16
+ return self.registry.get(adapter_id).generate(model, request)
@@ -0,0 +1,101 @@
1
+ """Materialize declarative provider configuration into executable adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from core.contracts.ai import AIProvider, ModelSpec
8
+ from core.contracts.model_runtime import ModelAdapter
9
+ from runtime.model.credentials import EnvironmentCredentialResolver
10
+ from runtime.model.native import (
11
+ AnthropicMessagesToolAdapter,
12
+ GeminiGenerateContentToolAdapter,
13
+ OpenAIResponsesToolAdapter,
14
+ )
15
+ from runtime.model.registry import ModelAdapterRegistry
16
+ from runtime.policy import ExecutionPolicy
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class ProviderMaterialization:
21
+ """Executable adapters plus non-secret credential checks."""
22
+
23
+ adapters: ModelAdapterRegistry
24
+ credential_checks: tuple[object, ...]
25
+
26
+
27
+ class NativeProviderMaterializer:
28
+ """Build provider-native adapters from AIProvider metadata.
29
+
30
+ Provider configuration remains declarative; this class is the only place that
31
+ maps provider kinds to concrete native adapters.
32
+ """
33
+
34
+ _CREDENTIALS = {
35
+ "openai": "OPENAI_API_KEY",
36
+ "anthropic": "ANTHROPIC_API_KEY",
37
+ "gemini": "GEMINI_API_KEY",
38
+ }
39
+
40
+ def __init__(
41
+ self,
42
+ *,
43
+ policy: ExecutionPolicy | None = None,
44
+ credentials: EnvironmentCredentialResolver | None = None,
45
+ ) -> None:
46
+ self.policy = policy or ExecutionPolicy()
47
+ self.credentials = credentials or EnvironmentCredentialResolver()
48
+
49
+ def materialize(
50
+ self,
51
+ providers: tuple[AIProvider, ...],
52
+ registry: ModelAdapterRegistry | None = None,
53
+ *,
54
+ validate_credentials: bool = False,
55
+ ) -> ProviderMaterialization:
56
+ target = registry or ModelAdapterRegistry()
57
+ checks = []
58
+ for provider in providers:
59
+ kind = provider.kind.lower()
60
+ adapter_id = str(provider.metadata.get("adapter_id", provider.id))
61
+ adapter = self._adapter(kind, provider)
62
+ target.register(adapter_id, adapter)
63
+ for model in provider.models:
64
+ model_adapter_id = str(model.metadata.get("adapter_id", adapter_id))
65
+ if model_adapter_id != adapter_id and model_adapter_id not in target.list():
66
+ target.register(model_adapter_id, adapter)
67
+ if validate_credentials:
68
+ env = str(metadata_env(provider, kind))
69
+ if env:
70
+ checks.extend(self.credentials.check([env]))
71
+ return ProviderMaterialization(target, tuple(checks))
72
+
73
+ def _adapter(self, kind: str, provider: AIProvider) -> ModelAdapter:
74
+ metadata = provider.metadata
75
+ policy = self.policy
76
+ if kind in {"openai", "openai_responses"}:
77
+ return OpenAIResponsesToolAdapter(
78
+ api_key_env=str(metadata.get("api_key_env", "OPENAI_API_KEY")),
79
+ endpoint=str(metadata.get("endpoint", "https://api.openai.com/v1/responses")),
80
+ policy=policy,
81
+ )
82
+ if kind in {"anthropic", "anthropic_messages"}:
83
+ return AnthropicMessagesToolAdapter(
84
+ api_key_env=str(metadata.get("api_key_env", "ANTHROPIC_API_KEY")),
85
+ endpoint=str(metadata.get("endpoint", "https://api.anthropic.com/v1/messages")),
86
+ api_version=str(metadata.get("api_version", "2023-06-01")),
87
+ max_tokens=int(metadata.get("max_tokens", 4096)),
88
+ policy=policy,
89
+ )
90
+ if kind in {"gemini", "gemini_generate_content"}:
91
+ return GeminiGenerateContentToolAdapter(
92
+ api_key_env=str(metadata.get("api_key_env", "GEMINI_API_KEY")),
93
+ endpoint_base=str(metadata.get("endpoint_base", "https://generativelanguage.googleapis.com/v1beta/models")),
94
+ policy=policy,
95
+ )
96
+ raise ValueError(f"Unsupported native provider kind: {provider.kind}")
97
+
98
+
99
+ def metadata_env(provider: AIProvider, kind: str) -> str:
100
+ default = NativeProviderMaterializer._CREDENTIALS.get(kind, "")
101
+ return str(provider.metadata.get("api_key_env", default))
@@ -0,0 +1,244 @@
1
+ """Native HTTP provider adapters for VYRELON Tool Calling."""
2
+ from __future__ import annotations
3
+ import json
4
+ import os
5
+ import re
6
+ import urllib.parse
7
+ import urllib.request
8
+ from urllib.error import HTTPError, URLError
9
+ from dataclasses import dataclass
10
+ from typing import Any
11
+ from core.contracts.ai import ModelSpec
12
+ from core.contracts.model_runtime import ModelRequest, ModelResponse
13
+ from core.contracts.vyrelon_runtime import ToolSpec
14
+ from runtime.policy import ExecutionPolicy
15
+
16
+ def _tool_name(name: str) -> str:
17
+ return re.sub(r"[^A-Za-z0-9_-]", "_", name)[:64] or "tool"
18
+
19
+ def _tool_defs(tools: tuple[ToolSpec, ...]) -> tuple[list[dict[str, Any]], dict[str, str]]:
20
+ aliases, used, out = {}, set(), []
21
+ for tool in tools:
22
+ alias, base = _tool_name(tool.id), _tool_name(tool.id)
23
+ index = 2
24
+ while alias in used:
25
+ alias = f"{base}_{index}"
26
+ index += 1
27
+ used.add(alias)
28
+ aliases[alias] = tool.id
29
+ out.append({"name": alias, "description": tool.description, "parameters": tool.input_schema or {"type": "object", "properties": {}}})
30
+ return out, aliases
31
+
32
+ def _request(endpoint: str, payload: dict[str, Any], headers: dict[str, str], policy: ExecutionPolicy) -> tuple[dict[str, Any], dict[str, str]]:
33
+ if not policy.permits("network"):
34
+ raise PermissionError("Network model execution is disabled by policy")
35
+ req = urllib.request.Request(endpoint, data=json.dumps(payload).encode(), headers={"Content-Type": "application/json", **headers}, method="POST")
36
+ try:
37
+ with urllib.request.urlopen(req, timeout=300) as response:
38
+ data = json.loads(response.read().decode())
39
+ response_headers = {str(k).lower(): str(v) for k, v in response.headers.items()}
40
+ except HTTPError as exc:
41
+ raise RuntimeError(f"provider request failed with HTTP {exc.code}") from exc
42
+ except URLError as exc:
43
+ raise RuntimeError("provider request failed: network error") from exc
44
+ if not isinstance(data, dict):
45
+ raise ValueError("provider response must be a JSON object")
46
+ return data, response_headers
47
+
48
+ @dataclass(frozen=True)
49
+ class OpenAIResponsesToolAdapter:
50
+ api_key_env: str = "OPENAI_API_KEY"
51
+ endpoint: str = "https://api.openai.com/v1/responses"
52
+ policy: ExecutionPolicy = ExecutionPolicy(allow_network=True)
53
+ def generate(self, model: ModelSpec, request: ModelRequest) -> ModelResponse:
54
+ return self._generate(model, request, ())
55
+ def generate_with_tools(self, model: ModelSpec, request: ModelRequest, tools: tuple[ToolSpec, ...]) -> ModelResponse:
56
+ return self._generate(model, request, tools)
57
+ def _generate(self, model: ModelSpec, request: ModelRequest, tools: tuple[ToolSpec, ...]) -> ModelResponse:
58
+ key = os.environ.get(self.api_key_env)
59
+ if not key: raise RuntimeError(f"Required model credential environment variable is missing: {self.api_key_env}")
60
+ definitions, aliases = _tool_defs(tools)
61
+ input_items: list[dict[str, Any]] = [{"role": "user", "content": request.prompt}]
62
+ history = request.metadata.get("tool_history", ())
63
+ for turn in history if isinstance(history, (list, tuple)) else ():
64
+ for call in turn.get("tool_calls", ()):
65
+ input_items.append({"type": "function_call", "call_id": call["call_id"], "name": _tool_name(str(call["tool_id"])), "arguments": json.dumps(call["arguments"])})
66
+ for result in turn.get("tool_results", ()):
67
+ input_items.append({"type": "function_call_output", "call_id": result["call_id"], "output": json.dumps(result.get("output")) if result.get("ok") else json.dumps({"error": result.get("error")})})
68
+ payload: dict[str, Any] = {"model": model.id, "input": input_items}
69
+ if request.system: payload["instructions"] = request.system
70
+ if definitions: payload["tools"] = [{"type": "function", "name": d["name"], "description": d["description"], "parameters": d["parameters"]} for d in definitions]
71
+ data, response_headers = _request(self.endpoint, payload, {"Authorization": f"Bearer {key}"}, self.policy)
72
+ calls, texts = [], []
73
+ for item in data.get("output", ()):
74
+ if item.get("type") == "function_call":
75
+ calls.append({"id": item.get("call_id", item.get("id")), "name": aliases.get(item.get("name"), item.get("name")), "arguments": json.loads(item.get("arguments", "{}"))})
76
+ elif item.get("type") == "message":
77
+ texts.extend(part.get("text", "") for part in item.get("content", ()) if part.get("type") == "output_text")
78
+ rate_limits = _openai_rate_limits(response_headers)
79
+ usage = _usage_dict(data.get("usage"))
80
+ metadata = {"adapter": "openai_responses", "tool_calls": calls, "provider_response_id": data.get("id")}
81
+ if rate_limits: metadata["rate_limits"] = rate_limits
82
+ if usage: metadata["usage"] = usage
83
+ return ModelResponse("".join(texts), model.id, metadata)
84
+
85
+ @dataclass(frozen=True)
86
+ class AnthropicMessagesToolAdapter:
87
+ api_key_env: str = "ANTHROPIC_API_KEY"
88
+ endpoint: str = "https://api.anthropic.com/v1/messages"
89
+ api_version: str = "2023-06-01"
90
+ max_tokens: int = 4096
91
+ policy: ExecutionPolicy = ExecutionPolicy(allow_network=True)
92
+ def generate(self, model: ModelSpec, request: ModelRequest) -> ModelResponse:
93
+ return self._generate(model, request, ())
94
+ def generate_with_tools(self, model: ModelSpec, request: ModelRequest, tools: tuple[ToolSpec, ...]) -> ModelResponse:
95
+ return self._generate(model, request, tools)
96
+ def _generate(self, model: ModelSpec, request: ModelRequest, tools: tuple[ToolSpec, ...]) -> ModelResponse:
97
+ key = os.environ.get(self.api_key_env)
98
+ if not key: raise RuntimeError(f"Required model credential environment variable is missing: {self.api_key_env}")
99
+ definitions, aliases = _tool_defs(tools)
100
+ messages: list[dict[str, Any]] = [{"role": "user", "content": request.prompt}]
101
+ history = request.metadata.get("tool_history", ())
102
+ for turn in history if isinstance(history, (list, tuple)) else ():
103
+ assistant = [{"type": "tool_use", "id": c["call_id"], "name": _tool_name(str(c["tool_id"])), "input": c["arguments"]} for c in turn.get("tool_calls", ())]
104
+ if assistant: messages.append({"role": "assistant", "content": assistant})
105
+ results = [{"type": "tool_result", "tool_use_id": r["call_id"], "content": json.dumps(r.get("output")) if r.get("ok") else json.dumps({"error": r.get("error")}), "is_error": not r.get("ok", False)} for r in turn.get("tool_results", ())]
106
+ if results: messages.append({"role": "user", "content": results})
107
+ payload: dict[str, Any] = {"model": model.id, "max_tokens": self.max_tokens, "messages": messages}
108
+ if request.system: payload["system"] = request.system
109
+ if definitions: payload["tools"] = [{"name": d["name"], "description": d["description"], "input_schema": d["parameters"]} for d in definitions]
110
+ data, response_headers = _request(self.endpoint, payload, {"x-api-key": key, "anthropic-version": self.api_version}, self.policy)
111
+ calls, texts = [], []
112
+ for block in data.get("content", ()):
113
+ if block.get("type") == "tool_use": calls.append({"id": block["id"], "name": aliases.get(block["name"], block["name"]), "arguments": block.get("input", {})})
114
+ elif block.get("type") == "text": texts.append(block.get("text", ""))
115
+ rate_limits = _anthropic_rate_limits(response_headers)
116
+ usage = _usage_dict(data.get("usage"))
117
+ metadata = {"adapter": "anthropic_messages", "tool_calls": calls, "stop_reason": data.get("stop_reason")}
118
+ if rate_limits: metadata["rate_limits"] = rate_limits
119
+ if usage: metadata["usage"] = usage
120
+ return ModelResponse("".join(texts), model.id, metadata)
121
+
122
+ @dataclass(frozen=True)
123
+ class GeminiGenerateContentToolAdapter:
124
+ api_key_env: str = "GEMINI_API_KEY"
125
+ endpoint_base: str = "https://generativelanguage.googleapis.com/v1beta/models"
126
+ policy: ExecutionPolicy = ExecutionPolicy(allow_network=True)
127
+ def generate(self, model: ModelSpec, request: ModelRequest) -> ModelResponse:
128
+ return self._generate(model, request, ())
129
+ def generate_with_tools(self, model: ModelSpec, request: ModelRequest, tools: tuple[ToolSpec, ...]) -> ModelResponse:
130
+ return self._generate(model, request, tools)
131
+ def _generate(self, model: ModelSpec, request: ModelRequest, tools: tuple[ToolSpec, ...]) -> ModelResponse:
132
+ key = os.environ.get(self.api_key_env)
133
+ if not key: raise RuntimeError(f"Required model credential environment variable is missing: {self.api_key_env}")
134
+ definitions, aliases = _tool_defs(tools)
135
+ contents: list[dict[str, Any]] = [{"role": "user", "parts": [{"text": request.prompt}]}]
136
+ history = request.metadata.get("tool_history", ())
137
+ for turn in history if isinstance(history, (list, tuple)) else ():
138
+ calls = [{"functionCall": {"name": _tool_name(str(c["tool_id"])), "args": c["arguments"]}} for c in turn.get("tool_calls", ())]
139
+ if calls: contents.append({"role": "model", "parts": calls})
140
+ results = [{"functionResponse": {"name": _tool_name(str(r["tool_id"])), "response": {"ok": r.get("ok"), "output": r.get("output"), "error": r.get("error")}}} for r in turn.get("tool_results", ())]
141
+ if results: contents.append({"role": "user", "parts": results})
142
+ payload: dict[str, Any] = {"contents": contents}
143
+ if request.system: payload["systemInstruction"] = {"parts": [{"text": request.system}]}
144
+ if definitions: payload["tools"] = [{"functionDeclarations": [{"name": d["name"], "description": d["description"], "parameters": d["parameters"]} for d in definitions]}]
145
+ endpoint = f"{self.endpoint_base}/{urllib.parse.quote(model.id, safe='')}:generateContent?key={urllib.parse.quote(key, safe='')}"
146
+ data, response_headers = _request(endpoint, payload, {}, self.policy)
147
+ calls, texts = [], []
148
+ for candidate in data.get("candidates", ()):
149
+ for part in candidate.get("content", {}).get("parts", ()):
150
+ if "functionCall" in part:
151
+ fc = part["functionCall"]; calls.append({"id": fc.get("id", f"call-{len(calls)+1}"), "name": aliases.get(fc.get("name"), fc.get("name")), "arguments": fc.get("args", {})})
152
+ elif "text" in part: texts.append(part["text"])
153
+ usage = _usage_dict(data.get("usageMetadata"))
154
+ metadata = {"adapter": "gemini_generate_content", "tool_calls": calls}
155
+ if usage: metadata["usage"] = usage
156
+ return ModelResponse("".join(texts), model.id, metadata)
157
+
158
+
159
+ def _usage_dict(value: object) -> dict[str, int]:
160
+ if not isinstance(value, dict):
161
+ return {}
162
+ aliases = {
163
+ "promptTokenCount": "input_tokens",
164
+ "inputTokenCount": "input_tokens",
165
+ "candidatesTokenCount": "output_tokens",
166
+ "outputTokenCount": "output_tokens",
167
+ "totalTokenCount": "total_tokens",
168
+ "thoughtsTokenCount": "thought_tokens",
169
+ "cachedContentTokenCount": "cached_tokens",
170
+ }
171
+ result: dict[str, int] = {}
172
+ for key, target in aliases.items():
173
+ raw = value.get(key)
174
+ if raw is not None:
175
+ try:
176
+ result[target] = int(raw)
177
+ except (TypeError, ValueError):
178
+ pass
179
+ for key in ("input_tokens", "output_tokens", "total_tokens"):
180
+ raw = value.get(key)
181
+ if raw is not None:
182
+ try:
183
+ result[key] = int(raw)
184
+ except (TypeError, ValueError):
185
+ pass
186
+ return result
187
+
188
+
189
+ def _openai_rate_limits(headers: dict[str, str]) -> dict[str, dict[str, object]]:
190
+ result: dict[str, dict[str, object]] = {}
191
+ pairs = (
192
+ ("requests", "x-ratelimit-limit-requests", "x-ratelimit-remaining-requests", "x-ratelimit-reset-requests"),
193
+ ("tokens", "x-ratelimit-limit-tokens", "x-ratelimit-remaining-tokens", "x-ratelimit-reset-tokens"),
194
+ )
195
+ for name, limit_key, remaining_key, reset_key in pairs:
196
+ if limit_key not in headers and remaining_key not in headers:
197
+ continue
198
+ result[name] = {
199
+ "limit": _header_number(headers.get(limit_key)),
200
+ "remaining": _header_number(headers.get(remaining_key)),
201
+ "reset_seconds": _reset_seconds(headers.get(reset_key)),
202
+ "source": "openai_response_headers",
203
+ }
204
+ return result
205
+
206
+
207
+ def _anthropic_rate_limits(headers: dict[str, str]) -> dict[str, dict[str, object]]:
208
+ result: dict[str, dict[str, object]] = {}
209
+ mappings = (
210
+ ("requests", "anthropic-ratelimit-requests-limit", "anthropic-ratelimit-requests-remaining", "anthropic-ratelimit-requests-reset"),
211
+ ("input_tokens", "anthropic-ratelimit-input-tokens-limit", "anthropic-ratelimit-input-tokens-remaining", "anthropic-ratelimit-input-tokens-reset"),
212
+ ("output_tokens", "anthropic-ratelimit-output-tokens-limit", "anthropic-ratelimit-output-tokens-remaining", "anthropic-ratelimit-output-tokens-reset"),
213
+ )
214
+ for name, limit_key, remaining_key, reset_key in mappings:
215
+ if limit_key not in headers and remaining_key not in headers:
216
+ continue
217
+ result[name] = {
218
+ "limit": _header_number(headers.get(limit_key)),
219
+ "remaining": _header_number(headers.get(remaining_key)),
220
+ "reset_at": headers.get(reset_key),
221
+ "source": "anthropic_response_headers",
222
+ }
223
+ return result
224
+
225
+
226
+ def _header_number(value: object) -> int | None:
227
+ try:
228
+ return int(float(str(value))) if value is not None else None
229
+ except (TypeError, ValueError):
230
+ return None
231
+
232
+
233
+ def _reset_seconds(value: object) -> float | None:
234
+ if value is None:
235
+ return None
236
+ text = str(value).strip()
237
+ try:
238
+ if text.endswith("ms"):
239
+ return float(text[:-2]) / 1000
240
+ if text.endswith("s"):
241
+ return float(text[:-1])
242
+ return float(text)
243
+ except ValueError:
244
+ return None