steerable-agent-runtime 0.7.4__tar.gz → 0.8.1__tar.gz
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.
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/PKG-INFO +2 -2
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/pyproject.toml +2 -2
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/__init__.py +10 -1
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/default.harness.json +5 -1
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/default.harness.yaml +9 -1
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/harness.py +18 -3
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/llm/anthropic_native.py +22 -6
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/llm/errors.py +50 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/llm/google_genai.py +16 -6
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/llm/openai_compat.py +18 -6
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/llm/openai_responses.py +16 -6
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/llm/system_proxy.py +133 -1
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/native_bridge.py +2 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/otel.py +127 -0
- steerable_agent_runtime-0.8.1/src/steerable_agent_runtime/retry.py +163 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime.egg-info/PKG-INFO +2 -2
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime.egg-info/SOURCES.txt +2 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime.egg-info/requires.txt +1 -1
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_otel.py +134 -0
- steerable_agent_runtime-0.8.1/tests/test_resume_failed_turn.py +291 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_retry_hooks.py +130 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_system_proxy.py +146 -0
- steerable_agent_runtime-0.8.1/tests/test_transport_error_detail.py +281 -0
- steerable_agent_runtime-0.7.4/src/steerable_agent_runtime/retry.py +0 -82
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/README.md +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/setup.cfg +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/antihallucination.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/approval.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/approval_policy.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/ask_user.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/branch.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/cache_control.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/calibration.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/compaction.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/compaction_policy.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/config.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/delegation_gate.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/errors.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/gateway_catalog.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/handoff.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/harness_spec.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/history.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/hooks.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/llm/__init__.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/llm/compat.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/llm/parts.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/llm/presets.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/loop.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/maintenance.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/mcp.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/mcp_server.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/model_catalog.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/model_info.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/model_resolve.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/observation_aging.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/orchestration.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/plugins.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/pool.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/pricing.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/pseudo.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/recording.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/reminders.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/replay.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/resume.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/sandboxed.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/skills.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/spill.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/storage/__init__.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/storage/sqlite_store.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/storage/write_lease.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/subagent.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/todo.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/tokens.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/tool_presentation.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/tool_schema.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/tool_search.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/tools.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/tracing.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/transport/__init__.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime/world_state.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_antihallucination.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_approval.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_approval_policy.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_ask_user.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_branch.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_cache_control.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_cache_instrumentation.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_calibration.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_compaction.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_compaction_policy.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_compaction_strategy.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_config.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_content_parts.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_delegation_gate.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_error_taxonomy.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_fragment_bounds.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_gateway_catalog.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_golden.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_handoff.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_harness.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_harness_spec.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_history.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_history_persistence.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_hooks.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_in_memory_storage.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_llm_gemini_wire.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_llm_responses_wire.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_llm_stream_e2e.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_llm_wire_helpers.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_long_session.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_loop.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_loop_cancellation.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_loop_replay.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_loop_sandbox_event.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_loop_terminal.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_maintenance.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_mcp.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_mcp_server.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_model_catalog.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_model_equivalence.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_model_info.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_model_resolve.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_native_default.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_observation_aging.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_orchestration.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_parallel_tools.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_plugins.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_provider_compat.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_provider_presets.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_pseudo.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_pyo3_api_surface.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_recording.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_reminders.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_replay_crosslang.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_resume.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_safety_gate.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_sandboxed.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_skills.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_soft_timeout.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_spill.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_sqlite_storage.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_steer.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_storage_contract.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_stream_strip.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_subagent.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_todo.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_todo_gate.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_tokens.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_tool_exposure.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_tool_hygiene.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_tool_presentation.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_tool_router.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_tool_schema.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_tool_timeout.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_trace_recorder.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_transport_jsonrpc.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_transport_sse.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_usage_attribution.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_world_state.py +0 -0
- {steerable_agent_runtime-0.7.4 → steerable_agent_runtime-0.8.1}/tests/test_write_lease.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: steerable-agent-runtime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -10,7 +10,7 @@ Requires-Dist: mcp<2.0.0,>=1.13.0
|
|
|
10
10
|
Requires-Dist: pydantic>=2.10.0
|
|
11
11
|
Requires-Dist: steerable-agent-protocol<1.0.0,>=0.1.0
|
|
12
12
|
Requires-Dist: steerable-agent-harness<1.0.0,>=0.1.0
|
|
13
|
-
Requires-Dist: steerable-agent-runtime-native==1.0.
|
|
13
|
+
Requires-Dist: steerable-agent-runtime-native==1.0.7
|
|
14
14
|
Provides-Extra: sqlalchemy
|
|
15
15
|
Requires-Dist: sqlalchemy>=2.0; extra == "sqlalchemy"
|
|
16
16
|
Provides-Extra: fastapi
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "steerable-agent-runtime"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.1"
|
|
4
4
|
description = "Steerable agent runtime: LLM, tool, storage, and transport adapters."
|
|
5
5
|
license = { text = "Apache-2.0" }
|
|
6
6
|
readme = "README.md"
|
|
@@ -21,7 +21,7 @@ dependencies = [
|
|
|
21
21
|
"pydantic>=2.10.0",
|
|
22
22
|
"steerable-agent-protocol>=0.1.0,<1.0.0",
|
|
23
23
|
"steerable-agent-harness>=0.1.0,<1.0.0",
|
|
24
|
-
"steerable-agent-runtime-native==1.0.
|
|
24
|
+
"steerable-agent-runtime-native==1.0.7",
|
|
25
25
|
]
|
|
26
26
|
|
|
27
27
|
[project.optional-dependencies]
|
|
@@ -193,7 +193,14 @@ from .orchestration import (
|
|
|
193
193
|
OrchestrationExecutor,
|
|
194
194
|
orchestration_tool_descriptors,
|
|
195
195
|
)
|
|
196
|
-
from .otel import
|
|
196
|
+
from .otel import (
|
|
197
|
+
PrivacyMode,
|
|
198
|
+
export_metrics,
|
|
199
|
+
export_otlp_http,
|
|
200
|
+
export_trace,
|
|
201
|
+
to_otlp_json,
|
|
202
|
+
to_otlp_metrics_json,
|
|
203
|
+
)
|
|
197
204
|
from .pricing import (
|
|
198
205
|
MODEL_PRICES,
|
|
199
206
|
ModelPrice,
|
|
@@ -462,6 +469,8 @@ __all__ = [
|
|
|
462
469
|
"estimate_tokens",
|
|
463
470
|
"export_otlp_http",
|
|
464
471
|
"export_trace",
|
|
472
|
+
"export_metrics",
|
|
473
|
+
"to_otlp_metrics_json",
|
|
465
474
|
"extract_inline_tool_calls",
|
|
466
475
|
"factor_for_model",
|
|
467
476
|
"family_tree",
|
|
@@ -13,8 +13,16 @@ retry:
|
|
|
13
13
|
- impl: informed_backtrack
|
|
14
14
|
# max_attempts 3 pins the production default (RetryHooks()); the strategy
|
|
15
15
|
# default of 4 would silently diverge the spec from the sidecar chat path.
|
|
16
|
+
# transport_* is the connection-failure policy: seconds-scale backoff under
|
|
17
|
+
# a wall-clock deadline, so a proxy or link outage is retried long enough to
|
|
18
|
+
# ride out a blip and fails within the deadline instead of after minutes.
|
|
16
19
|
- impl: simple
|
|
17
|
-
params:
|
|
20
|
+
params:
|
|
21
|
+
max_attempts: 3
|
|
22
|
+
transport_max_attempts: 5
|
|
23
|
+
transport_base_delay_ms: 1000
|
|
24
|
+
transport_max_delay_ms: 8000
|
|
25
|
+
transport_deadline_ms: 60000
|
|
18
26
|
# Quoted: bare `null` is YAML's null literal, not the implementation name.
|
|
19
27
|
validator: "null"
|
|
20
28
|
tools: full
|
|
@@ -27,7 +27,7 @@ from .compaction import CompactionHooks
|
|
|
27
27
|
from .history import ContextFragment
|
|
28
28
|
from .hooks import LoopHooks, NoopHooks
|
|
29
29
|
from .llm import LLMProvider
|
|
30
|
-
from .retry import RetryHooks
|
|
30
|
+
from .retry import RetryHooks, TransportRetryPolicy
|
|
31
31
|
from .storage import InMemoryStorage, StorageAdapter
|
|
32
32
|
from .tool_search import DEFAULT_MAX_RESULTS
|
|
33
33
|
from .tools import ToolRouter
|
|
@@ -329,11 +329,20 @@ class NoRetry:
|
|
|
329
329
|
|
|
330
330
|
@dataclass(frozen=True, slots=True)
|
|
331
331
|
class SimpleRetry:
|
|
332
|
-
"""
|
|
332
|
+
"""Bounded exponential backoff (RetryHooks).
|
|
333
|
+
|
|
334
|
+
The ``transport_*`` fields configure the connection-failure policy, which
|
|
335
|
+
is deliberately more patient than the shared one and stops at a deadline:
|
|
336
|
+
see :class:`TransportRetryPolicy`.
|
|
337
|
+
"""
|
|
333
338
|
|
|
334
339
|
max_attempts: int = 4
|
|
335
340
|
base_delay_ms: int = 200
|
|
336
341
|
max_delay_ms: int = 5000
|
|
342
|
+
transport_max_attempts: int = 5
|
|
343
|
+
transport_base_delay_ms: int = 1000
|
|
344
|
+
transport_max_delay_ms: int = 8000
|
|
345
|
+
transport_deadline_ms: int = 60_000
|
|
337
346
|
name: str = "simple"
|
|
338
347
|
assumes: str = (
|
|
339
348
|
"failures are dominated by transient transport/rate-limit/server "
|
|
@@ -350,7 +359,13 @@ class SimpleRetry:
|
|
|
350
359
|
max_attempts=self.max_attempts,
|
|
351
360
|
base_delay_ms=self.base_delay_ms,
|
|
352
361
|
max_delay_ms=self.max_delay_ms,
|
|
353
|
-
)
|
|
362
|
+
),
|
|
363
|
+
transport_policy=TransportRetryPolicy(
|
|
364
|
+
max_attempts=self.transport_max_attempts,
|
|
365
|
+
base_delay_ms=self.transport_base_delay_ms,
|
|
366
|
+
max_delay_ms=self.transport_max_delay_ms,
|
|
367
|
+
deadline_ms=self.transport_deadline_ms,
|
|
368
|
+
),
|
|
354
369
|
)
|
|
355
370
|
)
|
|
356
371
|
|
|
@@ -20,16 +20,19 @@ from typing import Any
|
|
|
20
20
|
from steerable_agent_protocol.generated import ToolCall
|
|
21
21
|
|
|
22
22
|
from . import LLMMessage, LLMStreamChunk, LLMUsage
|
|
23
|
-
from .errors import LLMError, classify_http_status
|
|
23
|
+
from .errors import LLMError, classify_http_status, describe_transport_error
|
|
24
24
|
from .parts import ImagePart, TextPart
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def _classify_anthropic_error(
|
|
27
|
+
def _classify_anthropic_error(
|
|
28
|
+
exc: Exception, *, provider: str, proxy: str | None = None
|
|
29
|
+
) -> Exception:
|
|
28
30
|
"""Normalize anthropic SDK exceptions into the ``LLMError`` taxonomy.
|
|
29
31
|
|
|
30
32
|
The SDK raises ``APIStatusError`` (has ``status_code``) for HTTP failures
|
|
31
33
|
and ``APIConnectionError`` for transport problems; anything already
|
|
32
|
-
classified (or unrecognized) passes through with ``unknown``.
|
|
34
|
+
classified (or unrecognized) passes through with ``unknown``. `proxy` is
|
|
35
|
+
the hop the SDK client used, reported on transport failures.
|
|
33
36
|
"""
|
|
34
37
|
if isinstance(exc, LLMError):
|
|
35
38
|
return exc
|
|
@@ -45,7 +48,10 @@ def _classify_anthropic_error(exc: Exception, *, provider: str) -> Exception:
|
|
|
45
48
|
# APIConnectionError / APITimeoutError have no status_code.
|
|
46
49
|
if exc.__class__.__name__ in ("APIConnectionError", "APITimeoutError"):
|
|
47
50
|
return LLMError(
|
|
48
|
-
f"{provider}: transport error: {exc}",
|
|
51
|
+
f"{provider}: transport error: {describe_transport_error(exc)}",
|
|
52
|
+
kind="transport",
|
|
53
|
+
provider=provider,
|
|
54
|
+
proxy=proxy,
|
|
49
55
|
)
|
|
50
56
|
return LLMError(f"{provider}: {exc}", kind="unknown", provider=provider)
|
|
51
57
|
|
|
@@ -79,7 +85,9 @@ class AnthropicProvider:
|
|
|
79
85
|
try:
|
|
80
86
|
message = await client.messages.create(**body)
|
|
81
87
|
except Exception as exc: # noqa: BLE001 - normalized into the taxonomy
|
|
82
|
-
raise _classify_anthropic_error(
|
|
88
|
+
raise _classify_anthropic_error(
|
|
89
|
+
exc, provider=self.name, proxy=self._egress_proxy()
|
|
90
|
+
) from exc
|
|
83
91
|
text_chunks: list[str] = []
|
|
84
92
|
tool_calls: list[ToolCall] = []
|
|
85
93
|
for block in message.content or []:
|
|
@@ -134,12 +142,20 @@ class AnthropicProvider:
|
|
|
134
142
|
finish_reason="stop",
|
|
135
143
|
)
|
|
136
144
|
except Exception as exc: # noqa: BLE001 - normalized into the taxonomy
|
|
137
|
-
raise _classify_anthropic_error(
|
|
145
|
+
raise _classify_anthropic_error(
|
|
146
|
+
exc, provider=self.name, proxy=self._egress_proxy()
|
|
147
|
+
) from exc
|
|
138
148
|
|
|
139
149
|
# ------------------------------------------------------------------
|
|
140
150
|
# Helpers
|
|
141
151
|
# ------------------------------------------------------------------
|
|
142
152
|
|
|
153
|
+
def _egress_proxy(self) -> str | None:
|
|
154
|
+
"""Proxy the SDK client reaches `base_url` through, for failure reports."""
|
|
155
|
+
from .system_proxy import egress_path
|
|
156
|
+
|
|
157
|
+
return egress_path(self.base_url or "https://api.anthropic.com").proxy
|
|
158
|
+
|
|
143
159
|
def _client(self) -> Any:
|
|
144
160
|
from anthropic import AsyncAnthropic # local import keeps optional dep optional
|
|
145
161
|
|
|
@@ -54,6 +54,7 @@ class LLMError(Exception):
|
|
|
54
54
|
provider: str | None = None,
|
|
55
55
|
raw: Any | None = None,
|
|
56
56
|
retry_after_ms: int | None = None,
|
|
57
|
+
proxy: str | None = None,
|
|
57
58
|
) -> None:
|
|
58
59
|
super().__init__(message)
|
|
59
60
|
self.kind = kind
|
|
@@ -61,12 +62,61 @@ class LLMError(Exception):
|
|
|
61
62
|
self.provider = provider
|
|
62
63
|
self.raw = raw
|
|
63
64
|
self.retry_after_ms = retry_after_ms
|
|
65
|
+
#: Proxy the request went through, credentials stripped; None means
|
|
66
|
+
#: the request went direct. A failure report shows this so the reader
|
|
67
|
+
#: knows which hop failed without having to reproduce the settings.
|
|
68
|
+
self.proxy = proxy
|
|
64
69
|
|
|
65
70
|
@property
|
|
66
71
|
def retryable(self) -> bool:
|
|
67
72
|
return self.kind in RETRYABLE_KINDS
|
|
68
73
|
|
|
69
74
|
|
|
75
|
+
#: Cause-chain walk depth for :func:`describe_transport_error`. httpx →
|
|
76
|
+
#: httpcore → anyio → ssl/socket is four; a little headroom costs nothing.
|
|
77
|
+
_MAX_CAUSE_DEPTH = 6
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def describe_transport_error(exc: BaseException) -> str:
|
|
81
|
+
"""Flatten a transport exception into one diagnosable string.
|
|
82
|
+
|
|
83
|
+
httpx reports a proxy that accepted ``CONNECT`` and then dropped the
|
|
84
|
+
tunnel as ``ConnectError('')`` — the empty message is httpcore's, and the
|
|
85
|
+
``ssl.SSLEOFError`` or ``socket.gaierror`` that explains it is three
|
|
86
|
+
``__cause__`` hops down. Logging ``str(exc)`` alone therefore produced
|
|
87
|
+
``transport error: `` with nothing after it. This walks the chain and
|
|
88
|
+
keeps every hop that names something, outermost first.
|
|
89
|
+
|
|
90
|
+
The walk follows ``__cause__``; it falls back to ``__context__`` only
|
|
91
|
+
when the raiser did not set ``__cause__`` and did not ask for the
|
|
92
|
+
context to be suppressed (``raise ... from None``), so an explicit
|
|
93
|
+
suppression does not leak the exception it hid.
|
|
94
|
+
|
|
95
|
+
Consecutive hops of the same class with no message each collapse into
|
|
96
|
+
one: httpcore re-wraps its own ``ConnectError`` on the proxy path, and
|
|
97
|
+
``ConnectError <- ConnectError`` names nothing the first hop did not.
|
|
98
|
+
|
|
99
|
+
A chain with no message at all still returns the class names, so the
|
|
100
|
+
line never ends in silence.
|
|
101
|
+
"""
|
|
102
|
+
parts: list[tuple[str, str]] = []
|
|
103
|
+
seen: set[int] = set()
|
|
104
|
+
current: BaseException | None = exc
|
|
105
|
+
while current is not None and len(seen) < _MAX_CAUSE_DEPTH:
|
|
106
|
+
if id(current) in seen:
|
|
107
|
+
break
|
|
108
|
+
seen.add(id(current))
|
|
109
|
+
name = type(current).__name__
|
|
110
|
+
text = str(current).strip()
|
|
111
|
+
if text or not parts or parts[-1] != (name, ""):
|
|
112
|
+
parts.append((name, text))
|
|
113
|
+
cause = current.__cause__
|
|
114
|
+
if cause is None and not current.__suppress_context__:
|
|
115
|
+
cause = current.__context__
|
|
116
|
+
current = cause if cause is not None and cause is not current else None
|
|
117
|
+
return " <- ".join(f"{name}: {text}" if text else name for name, text in parts)
|
|
118
|
+
|
|
119
|
+
|
|
70
120
|
#: Body substrings that mark a 400/413 as a context-window overflow rather
|
|
71
121
|
#: than a generic invalid request. Covers OpenAI, Ollama, vLLM, DeepSeek,
|
|
72
122
|
#: and Anthropic phrasings (matched case-insensitively).
|
|
@@ -23,10 +23,15 @@ from typing import Any, Literal
|
|
|
23
23
|
from steerable_agent_protocol.generated import ToolCall
|
|
24
24
|
|
|
25
25
|
from . import LLMMessage, LLMStreamChunk, LLMUsage
|
|
26
|
-
from .errors import
|
|
26
|
+
from .errors import (
|
|
27
|
+
LLMError,
|
|
28
|
+
classify_http_status,
|
|
29
|
+
describe_transport_error,
|
|
30
|
+
parse_retry_after_ms,
|
|
31
|
+
)
|
|
27
32
|
from .parts import ImagePart, TextPart
|
|
28
33
|
from .presets import ProviderPreset, preset_for
|
|
29
|
-
from .system_proxy import client_env_kwargs
|
|
34
|
+
from .system_proxy import client_env_kwargs, egress_path
|
|
30
35
|
|
|
31
36
|
logger = logging.getLogger(__name__)
|
|
32
37
|
|
|
@@ -79,6 +84,9 @@ class GoogleGenAIProvider:
|
|
|
79
84
|
base_url: str = "https://generativelanguage.googleapis.com"
|
|
80
85
|
api_key: str | None = None
|
|
81
86
|
default_temperature: float | None = None
|
|
87
|
+
#: Send this endpoint's requests direct even when the host declares a
|
|
88
|
+
#: proxy (see `OpenAICompatProvider.force_direct`).
|
|
89
|
+
force_direct: bool = False
|
|
82
90
|
preset: ProviderPreset | Literal["auto", "off"] = "auto"
|
|
83
91
|
|
|
84
92
|
def __post_init__(self) -> None:
|
|
@@ -110,7 +118,7 @@ class GoogleGenAIProvider:
|
|
|
110
118
|
try:
|
|
111
119
|
async with httpx.AsyncClient(
|
|
112
120
|
timeout=httpx.Timeout(60.0),
|
|
113
|
-
**client_env_kwargs(self.base_url),
|
|
121
|
+
**client_env_kwargs(self.base_url, force_direct=self.force_direct),
|
|
114
122
|
) as client:
|
|
115
123
|
response = await client.post(
|
|
116
124
|
self._url(stream=False),
|
|
@@ -124,9 +132,10 @@ class GoogleGenAIProvider:
|
|
|
124
132
|
payload = response.json()
|
|
125
133
|
except httpx.TransportError as exc:
|
|
126
134
|
raise LLMError(
|
|
127
|
-
f"{self.name}: transport error: {exc}",
|
|
135
|
+
f"{self.name}: transport error: {describe_transport_error(exc)}",
|
|
128
136
|
kind="transport",
|
|
129
137
|
provider=self.name,
|
|
138
|
+
proxy=None if self.force_direct else egress_path(self.base_url).proxy,
|
|
130
139
|
) from exc
|
|
131
140
|
|
|
132
141
|
return _decode_response(payload)
|
|
@@ -153,7 +162,7 @@ class GoogleGenAIProvider:
|
|
|
153
162
|
async with (
|
|
154
163
|
httpx.AsyncClient(
|
|
155
164
|
timeout=_stream_timeout(),
|
|
156
|
-
**client_env_kwargs(self.base_url),
|
|
165
|
+
**client_env_kwargs(self.base_url, force_direct=self.force_direct),
|
|
157
166
|
) as client,
|
|
158
167
|
client.stream(
|
|
159
168
|
"POST",
|
|
@@ -185,9 +194,10 @@ class GoogleGenAIProvider:
|
|
|
185
194
|
yield parsed
|
|
186
195
|
except httpx.TransportError as exc:
|
|
187
196
|
raise LLMError(
|
|
188
|
-
f"{self.name}: transport error: {exc}",
|
|
197
|
+
f"{self.name}: transport error: {describe_transport_error(exc)}",
|
|
189
198
|
kind="transport",
|
|
190
199
|
provider=self.name,
|
|
200
|
+
proxy=None if self.force_direct else egress_path(self.base_url).proxy,
|
|
191
201
|
) from exc
|
|
192
202
|
|
|
193
203
|
# ------------------------------------------------------------------
|
|
@@ -30,10 +30,15 @@ from ..model_info import (
|
|
|
30
30
|
)
|
|
31
31
|
from . import LLMMessage, LLMStreamChunk, LLMUsage
|
|
32
32
|
from .compat import OpenAICompatFlags
|
|
33
|
-
from .errors import
|
|
33
|
+
from .errors import (
|
|
34
|
+
LLMError,
|
|
35
|
+
classify_http_status,
|
|
36
|
+
describe_transport_error,
|
|
37
|
+
parse_retry_after_ms,
|
|
38
|
+
)
|
|
34
39
|
from .parts import ImagePart, TextPart
|
|
35
40
|
from .presets import ProviderPreset, preset_for
|
|
36
|
-
from .system_proxy import client_env_kwargs
|
|
41
|
+
from .system_proxy import client_env_kwargs, egress_path
|
|
37
42
|
|
|
38
43
|
logger = logging.getLogger(__name__)
|
|
39
44
|
|
|
@@ -145,6 +150,11 @@ class OpenAICompatProvider:
|
|
|
145
150
|
#: a ``ProviderPreset`` instance pins that preset regardless of the
|
|
146
151
|
#: registry (host settings UIs send an explicit choice this way).
|
|
147
152
|
preset: ProviderPreset | Literal["auto", "off"] = "auto"
|
|
153
|
+
#: Send this endpoint's requests direct even when the host declares a
|
|
154
|
+
#: proxy. Set for a model service the user redirected after a proxy turned
|
|
155
|
+
#: a working connection into a timeout; the proxy stays configured for
|
|
156
|
+
#: everything else.
|
|
157
|
+
force_direct: bool = False
|
|
148
158
|
#: Per-request reasoning effort from the host's model picker; wins over
|
|
149
159
|
#: ``STEERABLE_REASONING_EFFORT`` and the preset default. Explicitly
|
|
150
160
|
#: requested effort is validated strict (``clamp_reasoning_effort``) —
|
|
@@ -197,7 +207,7 @@ class OpenAICompatProvider:
|
|
|
197
207
|
try:
|
|
198
208
|
async with httpx.AsyncClient(
|
|
199
209
|
timeout=httpx.Timeout(60.0),
|
|
200
|
-
**client_env_kwargs(self.base_url),
|
|
210
|
+
**client_env_kwargs(self.base_url, force_direct=self.force_direct),
|
|
201
211
|
) as client:
|
|
202
212
|
response = await client.post(
|
|
203
213
|
f"{self.base_url.rstrip('/')}/chat/completions",
|
|
@@ -211,9 +221,10 @@ class OpenAICompatProvider:
|
|
|
211
221
|
payload = response.json()
|
|
212
222
|
except httpx.TransportError as exc:
|
|
213
223
|
raise LLMError(
|
|
214
|
-
f"{self.name}: transport error: {exc}",
|
|
224
|
+
f"{self.name}: transport error: {describe_transport_error(exc)}",
|
|
215
225
|
kind="transport",
|
|
216
226
|
provider=self.name,
|
|
227
|
+
proxy=None if self.force_direct else egress_path(self.base_url).proxy,
|
|
217
228
|
) from exc
|
|
218
229
|
|
|
219
230
|
choice = payload["choices"][0]
|
|
@@ -250,7 +261,7 @@ class OpenAICompatProvider:
|
|
|
250
261
|
async with (
|
|
251
262
|
httpx.AsyncClient(
|
|
252
263
|
timeout=_stream_timeout(),
|
|
253
|
-
**client_env_kwargs(self.base_url),
|
|
264
|
+
**client_env_kwargs(self.base_url, force_direct=self.force_direct),
|
|
254
265
|
) as client,
|
|
255
266
|
client.stream(
|
|
256
267
|
"POST",
|
|
@@ -306,9 +317,10 @@ class OpenAICompatProvider:
|
|
|
306
317
|
yield LLMStreamChunk(tool_call_delta=call)
|
|
307
318
|
except httpx.TransportError as exc:
|
|
308
319
|
raise LLMError(
|
|
309
|
-
f"{self.name}: transport error: {exc}",
|
|
320
|
+
f"{self.name}: transport error: {describe_transport_error(exc)}",
|
|
310
321
|
kind="transport",
|
|
311
322
|
provider=self.name,
|
|
323
|
+
proxy=None if self.force_direct else egress_path(self.base_url).proxy,
|
|
312
324
|
) from exc
|
|
313
325
|
|
|
314
326
|
# ------------------------------------------------------------------
|
|
@@ -25,10 +25,15 @@ from steerable_agent_protocol.generated import ToolCall
|
|
|
25
25
|
|
|
26
26
|
from ..model_info import ReasoningEffortUnsupported, clamp_reasoning_effort
|
|
27
27
|
from . import LLMMessage, LLMStreamChunk, LLMUsage
|
|
28
|
-
from .errors import
|
|
28
|
+
from .errors import (
|
|
29
|
+
LLMError,
|
|
30
|
+
classify_http_status,
|
|
31
|
+
describe_transport_error,
|
|
32
|
+
parse_retry_after_ms,
|
|
33
|
+
)
|
|
29
34
|
from .parts import ImagePart, TextPart
|
|
30
35
|
from .presets import ProviderPreset, preset_for
|
|
31
|
-
from .system_proxy import client_env_kwargs
|
|
36
|
+
from .system_proxy import client_env_kwargs, egress_path
|
|
32
37
|
|
|
33
38
|
logger = logging.getLogger(__name__)
|
|
34
39
|
|
|
@@ -75,6 +80,9 @@ class OpenAIResponsesProvider:
|
|
|
75
80
|
api_key: str | None = None
|
|
76
81
|
default_temperature: float | None = None
|
|
77
82
|
preset: ProviderPreset | Literal["auto", "off"] = "auto"
|
|
83
|
+
#: Send this endpoint's requests direct even when the host declares a
|
|
84
|
+
#: proxy (see `OpenAICompatProvider.force_direct`).
|
|
85
|
+
force_direct: bool = False
|
|
78
86
|
#: Per-request reasoning effort from the host's model picker; wins over
|
|
79
87
|
#: ``STEERABLE_REASONING_EFFORT`` and the preset default. Explicitly
|
|
80
88
|
#: requested effort is validated strict (``clamp_reasoning_effort``) —
|
|
@@ -112,7 +120,7 @@ class OpenAIResponsesProvider:
|
|
|
112
120
|
try:
|
|
113
121
|
async with httpx.AsyncClient(
|
|
114
122
|
timeout=httpx.Timeout(60.0),
|
|
115
|
-
**client_env_kwargs(self.base_url),
|
|
123
|
+
**client_env_kwargs(self.base_url, force_direct=self.force_direct),
|
|
116
124
|
) as client:
|
|
117
125
|
response = await client.post(
|
|
118
126
|
f"{self.base_url.rstrip('/')}/responses",
|
|
@@ -126,9 +134,10 @@ class OpenAIResponsesProvider:
|
|
|
126
134
|
payload = response.json()
|
|
127
135
|
except httpx.TransportError as exc:
|
|
128
136
|
raise LLMError(
|
|
129
|
-
f"{self.name}: transport error: {exc}",
|
|
137
|
+
f"{self.name}: transport error: {describe_transport_error(exc)}",
|
|
130
138
|
kind="transport",
|
|
131
139
|
provider=self.name,
|
|
140
|
+
proxy=None if self.force_direct else egress_path(self.base_url).proxy,
|
|
132
141
|
) from exc
|
|
133
142
|
|
|
134
143
|
if payload.get("status") == "failed" or payload.get("error"):
|
|
@@ -163,7 +172,7 @@ class OpenAIResponsesProvider:
|
|
|
163
172
|
async with (
|
|
164
173
|
httpx.AsyncClient(
|
|
165
174
|
timeout=_stream_timeout(),
|
|
166
|
-
**client_env_kwargs(self.base_url),
|
|
175
|
+
**client_env_kwargs(self.base_url, force_direct=self.force_direct),
|
|
167
176
|
) as client,
|
|
168
177
|
client.stream(
|
|
169
178
|
"POST",
|
|
@@ -207,9 +216,10 @@ class OpenAIResponsesProvider:
|
|
|
207
216
|
yield LLMStreamChunk(tool_call_delta=call)
|
|
208
217
|
except httpx.TransportError as exc:
|
|
209
218
|
raise LLMError(
|
|
210
|
-
f"{self.name}: transport error: {exc}",
|
|
219
|
+
f"{self.name}: transport error: {describe_transport_error(exc)}",
|
|
211
220
|
kind="transport",
|
|
212
221
|
provider=self.name,
|
|
222
|
+
proxy=None if self.force_direct else egress_path(self.base_url).proxy,
|
|
213
223
|
) from exc
|
|
214
224
|
|
|
215
225
|
# ------------------------------------------------------------------
|
|
@@ -46,7 +46,8 @@ import os
|
|
|
46
46
|
import urllib.request
|
|
47
47
|
from collections.abc import Callable, Mapping
|
|
48
48
|
from functools import lru_cache
|
|
49
|
-
from
|
|
49
|
+
from typing import NamedTuple
|
|
50
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
50
51
|
|
|
51
52
|
#: The desktop sets this alongside the proxy variables when it confines the
|
|
52
53
|
#: sidecar's egress to its per-host proxy, and never on the startup-failure
|
|
@@ -117,6 +118,22 @@ def direct_mounts(
|
|
|
117
118
|
return _pin_direct(host)
|
|
118
119
|
|
|
119
120
|
|
|
121
|
+
@lru_cache(maxsize=1)
|
|
122
|
+
def _platform_proxies() -> tuple[tuple[str, str], ...]:
|
|
123
|
+
"""The platform's own proxy map, read once per process.
|
|
124
|
+
|
|
125
|
+
`urllib.request.getproxies` reaches System Configuration on macOS and the
|
|
126
|
+
registry on Windows; a failure report must not pay that round trip on
|
|
127
|
+
every request.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
return tuple(
|
|
131
|
+
(key, value) for key, value in urllib.request.getproxies().items() if value
|
|
132
|
+
)
|
|
133
|
+
except Exception:
|
|
134
|
+
return ()
|
|
135
|
+
|
|
136
|
+
|
|
120
137
|
def _egress_is_confined() -> bool:
|
|
121
138
|
return (os.environ.get(_EGRESS_CONFINED_ENV) or "").strip() == "1"
|
|
122
139
|
|
|
@@ -136,9 +153,114 @@ def _unbuildable_proxy_schemes(
|
|
|
136
153
|
return missing
|
|
137
154
|
|
|
138
155
|
|
|
156
|
+
class EgressPath(NamedTuple):
|
|
157
|
+
"""How a provider request to `base_url` leaves this process.
|
|
158
|
+
|
|
159
|
+
`proxy` is the client-side proxy address without credentials, or None for a
|
|
160
|
+
direct connection. It is what a failure report shows: the address alone
|
|
161
|
+
tells the reader which hop to inspect, while credentials would leak into
|
|
162
|
+
logs and copied diagnostics.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
proxy: str | None
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def uses_proxy(self) -> bool:
|
|
169
|
+
return self.proxy is not None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _redacted_proxy_url(value: str) -> str:
|
|
173
|
+
"""Proxy URL without userinfo; a bare `host:port` is returned as written."""
|
|
174
|
+
candidate = value if "://" in value else f"http://{value}"
|
|
175
|
+
parts = urlsplit(candidate)
|
|
176
|
+
host = parts.netloc.rpartition("@")[2]
|
|
177
|
+
return urlunsplit((parts.scheme, host, parts.path, "", ""))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _proxy_for_scheme(
|
|
181
|
+
proxies: Mapping[str, str],
|
|
182
|
+
*,
|
|
183
|
+
scheme: str,
|
|
184
|
+
env_bypass: Callable[[str], bool],
|
|
185
|
+
platform_list_bypass: Callable[[str], bool],
|
|
186
|
+
host: str | None,
|
|
187
|
+
) -> str | None:
|
|
188
|
+
"""The declared proxy `host` would use, or None when it is exempt."""
|
|
189
|
+
if host and (env_bypass(host) or platform_list_bypass(host)):
|
|
190
|
+
return None
|
|
191
|
+
for key in (scheme, "all"):
|
|
192
|
+
declared = proxies.get(key)
|
|
193
|
+
if declared:
|
|
194
|
+
return _redacted_proxy_url(declared)
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def egress_path(
|
|
199
|
+
base_url: str,
|
|
200
|
+
*,
|
|
201
|
+
env_proxies: Callable[[], Mapping[str, str]] = urllib.request.getproxies_environment,
|
|
202
|
+
platform_proxies: Callable[[], Mapping[str, str]] = lambda: dict(_platform_proxies()),
|
|
203
|
+
platform_bypass: Callable[[str], bool] = _platform_says_direct,
|
|
204
|
+
env_bypass: Callable[[str], bool] = urllib.request.proxy_bypass_environment,
|
|
205
|
+
platform_list_bypass: Callable[[str], bool] = _platform_list_says_direct,
|
|
206
|
+
confined: Callable[[], bool] = _egress_is_confined,
|
|
207
|
+
) -> EgressPath:
|
|
208
|
+
"""Which hop a request to `base_url` takes, for failure reports.
|
|
209
|
+
|
|
210
|
+
Mirrors `client_env_kwargs` rather than re-deciding: environment proxies
|
|
211
|
+
first, the platform's own proxy map when the environment declares none
|
|
212
|
+
(httpx reads `getproxies()` under `trust_env`), the confinement marker,
|
|
213
|
+
and the two bypass lists. It answers only what a report should show.
|
|
214
|
+
"""
|
|
215
|
+
host = urlsplit(base_url).hostname
|
|
216
|
+
scheme = urlsplit(base_url).scheme or "https"
|
|
217
|
+
proxies = env_proxies()
|
|
218
|
+
if not proxies:
|
|
219
|
+
if direct_mounts(
|
|
220
|
+
base_url, env_proxies=lambda: proxies, platform_bypass=platform_bypass
|
|
221
|
+
):
|
|
222
|
+
return EgressPath(proxy=None)
|
|
223
|
+
proxies = platform_proxies()
|
|
224
|
+
if confined():
|
|
225
|
+
declared = next(iter(proxies.values()), "")
|
|
226
|
+
return EgressPath(proxy=_redacted_proxy_url(declared) if declared else None)
|
|
227
|
+
return EgressPath(
|
|
228
|
+
proxy=_proxy_for_scheme(
|
|
229
|
+
proxies,
|
|
230
|
+
scheme=scheme,
|
|
231
|
+
env_bypass=env_bypass,
|
|
232
|
+
platform_list_bypass=platform_list_bypass,
|
|
233
|
+
host=host,
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _direct_pin_or_trust_env(
|
|
239
|
+
base_url: str,
|
|
240
|
+
proxies: Mapping[str, str],
|
|
241
|
+
*,
|
|
242
|
+
has_module: Callable[[str], bool],
|
|
243
|
+
) -> dict[str, None] | None:
|
|
244
|
+
"""A direct pin for `base_url`, or None when the pin cannot be built.
|
|
245
|
+
|
|
246
|
+
httpx builds a transport for every declared proxy while constructing the
|
|
247
|
+
client, so one unbuildable scheme fails the whole client no matter which
|
|
248
|
+
pattern the request matches. When that happens the caller must fall back to
|
|
249
|
+
`trust_env=False`, which is the only way to keep this host out of a proxy
|
|
250
|
+
map that cannot be assembled.
|
|
251
|
+
"""
|
|
252
|
+
host = urlsplit(base_url).hostname
|
|
253
|
+
if not host:
|
|
254
|
+
return None
|
|
255
|
+
if proxies and _unbuildable_proxy_schemes(proxies, has_module=has_module):
|
|
256
|
+
return None
|
|
257
|
+
return _pin_direct(host)
|
|
258
|
+
|
|
259
|
+
|
|
139
260
|
def client_env_kwargs(
|
|
140
261
|
base_url: str,
|
|
141
262
|
*,
|
|
263
|
+
force_direct: bool = False,
|
|
142
264
|
env_proxies: Callable[[], Mapping[str, str]] = urllib.request.getproxies_environment,
|
|
143
265
|
platform_bypass: Callable[[str], bool] = _platform_says_direct,
|
|
144
266
|
env_bypass: Callable[[str], bool] = urllib.request.proxy_bypass_environment,
|
|
@@ -157,7 +279,17 @@ def client_env_kwargs(
|
|
|
157
279
|
|
|
158
280
|
An empty dict leaves httpx's own environment handling untouched: the endpoint
|
|
159
281
|
is proxied as declared, or confined egress owns every host.
|
|
282
|
+
|
|
283
|
+
`force_direct` overrides all of that for one endpoint: the user asked this
|
|
284
|
+
model service to connect directly, typically after a proxy turned a working
|
|
285
|
+
connection into a timeout. It cannot override confined egress — that
|
|
286
|
+
declaration owns every host, and the host's own sandbox decides reachability
|
|
287
|
+
— so it resolves to `trust_env=False` there, which is what keeps the
|
|
288
|
+
per-host egress proxy out of the request.
|
|
160
289
|
"""
|
|
290
|
+
if force_direct:
|
|
291
|
+
pinned = _direct_pin_or_trust_env(base_url, env_proxies(), has_module=has_module)
|
|
292
|
+
return {"trust_env": False} if pinned is None else {"mounts": pinned}
|
|
161
293
|
proxies = env_proxies()
|
|
162
294
|
if not proxies:
|
|
163
295
|
mounts = direct_mounts(
|
|
@@ -308,6 +308,7 @@ async def _collect_stream(
|
|
|
308
308
|
status_code=getattr(exc, "status_code", None),
|
|
309
309
|
provider=getattr(exc, "provider", None),
|
|
310
310
|
retry_after_ms=getattr(exc, "retry_after_ms", None),
|
|
311
|
+
proxy=getattr(exc, "proxy", None),
|
|
311
312
|
)
|
|
312
313
|
return
|
|
313
314
|
display_tail = stripper.feed(content_carry) + stripper.flush()
|
|
@@ -470,6 +471,7 @@ async def _on_request_error(
|
|
|
470
471
|
status_code=error_raw.get("status_code"),
|
|
471
472
|
provider=error_raw.get("provider"),
|
|
472
473
|
retry_after_ms=error_raw.get("retry_after_ms"),
|
|
474
|
+
proxy=error_raw.get("proxy"),
|
|
473
475
|
)
|
|
474
476
|
transcript = [_msg_from_json(item) for item in raw.get("transcript", [])]
|
|
475
477
|
loop._run_context.round_index = int(raw.get("round_index") or 0)
|