agentengine-sdk-python 0.8.4__py3-none-any.whl → 0.8.6__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.
- {agentengine_sdk_python-0.8.4.dist-info → agentengine_sdk_python-0.8.6.dist-info}/METADATA +17 -10
- agentengine_sdk_python-0.8.6.dist-info/RECORD +1731 -0
- ksadk/_async_iterators.py +79 -0
- ksadk/_build_provenance.json +1 -1
- ksadk/_model_anthropic.py +49 -0
- ksadk/_model_input.py +86 -0
- ksadk/_model_json.py +40 -0
- ksadk/_model_reasoning.py +17 -0
- ksadk/_model_refusal.py +20 -0
- ksadk/_model_sse.py +163 -0
- ksadk/_process.py +83 -0
- ksadk/_str_enum.py +17 -0
- ksadk/a2a/__init__.py +157 -167
- ksadk/a2a/_invocation_state.py +86 -0
- ksadk/a2a/_space_client_events.py +33 -22
- ksadk/a2a/adk.py +194 -0
- ksadk/a2a/adk_invocation_context.py +242 -0
- ksadk/a2a/adk_runtime.py +126 -0
- ksadk/a2a/adk_tools.py +319 -0
- ksadk/a2a/bootstrap.py +149 -23
- ksadk/a2a/build_validation.py +147 -0
- ksadk/a2a/card.py +23 -15
- ksadk/a2a/codex.py +160 -0
- ksadk/a2a/codex_checkpoint.py +111 -0
- ksadk/a2a/codex_recovery.py +96 -0
- ksadk/a2a/codex_recovery_events.py +123 -0
- ksadk/a2a/codex_runtime.py +198 -0
- ksadk/a2a/codex_session_checkpoint.py +162 -0
- ksadk/a2a/composition.py +88 -0
- ksadk/a2a/context_store.py +118 -19
- ksadk/a2a/control_plane.py +108 -106
- ksadk/a2a/event_adapter.py +92 -193
- ksadk/a2a/executor.py +257 -85
- ksadk/a2a/exposure.py +106 -0
- ksadk/a2a/external_transport.py +3 -4
- ksadk/a2a/identity.py +106 -12
- ksadk/a2a/ids.py +14 -3
- ksadk/a2a/inbound_kernel.py +133 -0
- ksadk/a2a/interaction_wire.py +76 -0
- ksadk/a2a/invocation_checkpoint.py +274 -0
- ksadk/a2a/invocation_policy.py +87 -0
- ksadk/a2a/invocation_publication.py +210 -0
- ksadk/a2a/invocation_service.py +972 -0
- ksadk/a2a/invocation_types.py +459 -0
- ksadk/a2a/kernel_task_adapter.py +259 -0
- ksadk/a2a/langgraph.py +20 -7
- ksadk/a2a/langgraph_invocation_context.py +136 -0
- ksadk/a2a/langgraph_managed.py +150 -0
- ksadk/a2a/langgraph_recovery.py +32 -0
- ksadk/a2a/langgraph_runtime.py +89 -0
- ksadk/a2a/langgraph_tool_runtime.py +132 -0
- ksadk/a2a/langgraph_tools.py +63 -0
- ksadk/a2a/message_digest.py +40 -0
- ksadk/a2a/platform.py +389 -0
- ksadk/a2a/policy_refs.py +13 -0
- ksadk/a2a/presentation.py +402 -0
- ksadk/a2a/product_kernel_facts.py +177 -0
- ksadk/a2a/product_key.py +99 -0
- ksadk/a2a/protocol_errors.py +116 -0
- ksadk/a2a/result_text.py +49 -0
- ksadk/a2a/resume_store.py +93 -3
- ksadk/a2a/routes.py +6 -1
- ksadk/a2a/runtime_approval.py +198 -0
- ksadk/a2a/runtime_input.py +151 -0
- ksadk/a2a/server.py +16 -7
- ksadk/a2a/space_client.py +101 -16
- ksadk/a2a/task_adapter.py +19 -5
- ksadk/a2a/task_event_dispatcher.py +39 -2
- ksadk/a2a/task_event_outbox.py +215 -0
- ksadk/a2a/task_event_outbox_owned.py +130 -0
- ksadk/a2a/task_store.py +37 -0
- ksadk/a2a/tool_activity.py +176 -0
- ksadk/a2a/workload_tokens.py +134 -0
- ksadk/a2ui/core.py +3 -3
- ksadk/api/client.py +38 -79
- ksadk/api/sse_stream.py +96 -0
- ksadk/builders/code_builder.py +121 -118
- ksadk/builders/container_builder.py +11 -29
- ksadk/builders/framework_requirements.py +22 -2
- ksadk/builders/managed_runtime_builder.py +12 -0
- ksadk/builders/source_provenance.py +87 -0
- ksadk/cli/__init__.py +36 -0
- ksadk/cli/cmd_a2a.py +43 -0
- ksadk/cli/cmd_build.py +9 -0
- ksadk/cli/cmd_config.py +4 -4
- ksadk/cli/cmd_create.py +12 -12
- ksadk/cli/cmd_deploy.py +27 -2
- ksadk/cli/cmd_hermes.py +24 -2
- ksadk/cli/cmd_invoke.py +64 -88
- ksadk/cli/cmd_openclaw.py +26 -1
- ksadk/cli/cmd_plugin.py +70 -12
- ksadk/cli/cmd_studio.py +69 -17
- ksadk/cli/cmd_web.py +2 -0
- ksadk/cli/hosted_invoke.py +206 -0
- ksadk/cli/invoke_content.py +60 -0
- ksadk/cli/openclaw_env.py +1 -1
- ksadk/cli/resource_common.py +26 -0
- ksadk/cli/runtime_bootstrap.py +27 -5
- ksadk/codex/_expert_request_reader.py +48 -0
- ksadk/codex/a2a_tools.py +140 -0
- ksadk/codex/async_approval.py +169 -0
- ksadk/codex/async_client.py +938 -0
- ksadk/codex/async_proxy.py +216 -0
- ksadk/codex/base_runtime.py +25 -0
- ksadk/codex/client.py +3 -1047
- ksadk/codex/client_payloads.py +85 -0
- ksadk/codex/rollout_checkpoint.py +264 -0
- ksadk/codex/runtime.py +290 -54
- ksadk/common/auth.py +2 -2
- ksadk/common/cloud_headers.py +36 -0
- ksadk/common/diagnostics.py +34 -0
- ksadk/common/kop_client.py +2 -1
- ksadk/configs/__init__.py +1 -1
- ksadk/configs/env_registry.py +216 -143
- ksadk/configs/env_registry_a2a.py +173 -0
- ksadk/configs/env_registry_studio.py +134 -0
- ksadk/configs/global_config.py +2 -1
- ksadk/configs/settings.py +9 -3
- ksadk/connector/__init__.py +17 -0
- ksadk/connector/channel.py +477 -0
- ksadk/context_engine/assembler.py +9 -0
- ksadk/context_engine/capabilities.py +52 -4
- ksadk/context_engine/contributors.py +80 -17
- ksadk/context_engine/instruction_authority.py +18 -0
- ksadk/conversations/attachment_storage.py +10 -3
- ksadk/conversations/attachments.py +69 -20
- ksadk/conversations/contracts.py +28 -9
- ksadk/conversations/message_projection.py +51 -155
- ksadk/conversations/message_projection_blocks.py +137 -0
- ksadk/conversations/model_context.py +11 -3
- ksadk/conversations/normalize.py +28 -10
- ksadk/conversations/projector.py +63 -0
- ksadk/conversations/runtime_baseline.py +37 -0
- ksadk/conversations/runtime_compaction.py +7 -2
- ksadk/conversations/runtime_input.py +16 -4
- ksadk/conversations/runtime_invocation.py +17 -4
- ksadk/conversations/runtime_observability.py +39 -10
- ksadk/conversations/runtime_payloads.py +12 -0
- ksadk/conversations/runtime_persistence.py +296 -32
- ksadk/conversations/runtime_preparation.py +98 -13
- ksadk/conversations/runtime_projection.py +217 -0
- ksadk/conversations/runtime_resume.py +141 -70
- ksadk/conversations/runtime_stream_events.py +29 -35
- ksadk/conversations/runtime_streaming.py +8 -0
- ksadk/conversations/tool_receipt_resume.py +387 -0
- ksadk/conversations/tool_receipts.py +32 -0
- ksadk/deployment/env_forward.py +5 -0
- ksadk/deployment/manager.py +15 -6
- ksadk/deployment/providers/serverless.py +76 -136
- ksadk/deployment/providers/serverless_payloads.py +122 -0
- ksadk/evaluation/agent_eval_client.py +2 -1
- ksadk/evaluation/contracts.py +78 -3
- ksadk/evaluation/evaluators.py +166 -1
- ksadk/evaluation/evidence.py +271 -4
- ksadk/evaluation/local_adapter.py +76 -12
- ksadk/evaluation/studio_build_adapter.py +73 -9
- ksadk/events/adapters/_a2a_checkpoint.py +151 -0
- ksadk/events/adapters/_a2a_snapshot.py +18 -14
- ksadk/events/adapters/_a2a_support.py +15 -3
- ksadk/events/adapters/_a2a_tool_activity.py +123 -0
- ksadk/events/adapters/_codex_errors.py +49 -0
- ksadk/events/adapters/_codex_items.py +16 -8
- ksadk/events/adapters/_langgraph_support.py +46 -1
- ksadk/events/adapters/a2a.py +54 -26
- ksadk/events/adapters/adk.py +188 -30
- ksadk/events/adapters/codex.py +33 -7
- ksadk/events/adapters/langgraph.py +16 -6
- ksadk/events/canonical.py +5 -0
- ksadk/events/canonical_store.py +200 -4
- ksadk/events/reducer.py +82 -0
- ksadk/events/run_authority.py +176 -0
- ksadk/events/session_event.py +2 -2
- ksadk/harness/__init__.py +18 -0
- ksadk/harness/app.py +11 -0
- ksadk/harness/artifact_adapters/__init__.py +1 -0
- ksadk/harness/artifact_adapters/native_page_budget.py +172 -0
- ksadk/harness/artifact_adapters/native_search.py +249 -0
- ksadk/harness/artifact_adapters/native_search_budget.py +134 -0
- ksadk/harness/artifact_adapters/native_store.py +115 -0
- ksadk/harness/artifact_adapters/native_tool.py +222 -0
- ksadk/harness/artifact_store.py +230 -0
- ksadk/harness/browser_adapters/__init__.py +6 -0
- ksadk/harness/browser_adapters/native_browser_tool.py +92 -0
- ksadk/harness/browser_adapters/native_dom_snapshot.py +187 -0
- ksadk/harness/browser_adapters/native_playwright.py +953 -0
- ksadk/harness/browser_adapters/native_vision.py +295 -0
- ksadk/harness/capabilities.py +115 -0
- ksadk/harness/capability_adapters/__init__.py +1 -0
- ksadk/harness/capability_adapters/native_dsh.py +289 -0
- ksadk/harness/capability_runtime.py +332 -0
- ksadk/harness/closure_contract.py +92 -0
- ksadk/harness/compaction_record.py +153 -0
- ksadk/harness/compiler.py +284 -0
- ksadk/harness/config.py +28 -5
- ksadk/harness/conformance/__init__.py +33 -0
- ksadk/harness/conformance/contract.py +504 -0
- ksadk/harness/conformance/fixtures.py +102 -0
- ksadk/harness/context_engine.py +388 -0
- ksadk/harness/context_maintenance.py +259 -0
- ksadk/harness/context_manifest.py +188 -0
- ksadk/harness/context_sections.py +397 -0
- ksadk/harness/context_tuning.py +168 -0
- ksadk/harness/diagnostic_adapters/__init__.py +1 -0
- ksadk/harness/diagnostic_adapters/contracts.py +70 -0
- ksadk/harness/diagnostic_adapters/lsp_rpc.py +210 -0
- ksadk/harness/diagnostic_adapters/native_lsp.py +286 -0
- ksadk/harness/draft_runtime.py +322 -0
- ksadk/harness/engine/__init__.py +40 -0
- ksadk/harness/engine/base.py +122 -0
- ksadk/harness/engine/budgets.py +108 -0
- ksadk/harness/engine/capability_declarations.py +59 -0
- ksadk/harness/engine/completion_guard.py +57 -0
- ksadk/harness/engine/context_pipeline.py +1351 -0
- ksadk/harness/engine/graph_builder.py +352 -0
- ksadk/harness/engine/hooked_reasoner.py +380 -0
- ksadk/harness/engine/langgraph.py +1049 -0
- ksadk/harness/engine/mcp_disclosure.py +701 -0
- ksadk/harness/engine/phases/__init__.py +6 -0
- ksadk/harness/engine/phases/context.py +48 -0
- ksadk/harness/engine/phases/interaction.py +31 -0
- ksadk/harness/engine/phases/reason.py +552 -0
- ksadk/harness/engine/phases/tools.py +638 -0
- ksadk/harness/engine/policy_runtime.py +81 -0
- ksadk/harness/engine/postgres_checkpointer.py +386 -0
- ksadk/harness/engine/run_control_bridge.py +100 -0
- ksadk/harness/engine/run_types.py +98 -0
- ksadk/harness/engine/skill_disclosure.py +275 -0
- ksadk/harness/engine/spans.py +41 -0
- ksadk/harness/engine/steering.py +177 -0
- ksadk/harness/engine/subagents.py +360 -0
- ksadk/harness/engine/thread_ids.py +112 -0
- ksadk/harness/engine/tool_dispatch.py +249 -0
- ksadk/harness/event_tree.py +212 -0
- ksadk/harness/events.py +498 -0
- ksadk/harness/execution_policy.py +104 -0
- ksadk/harness/hooks.py +259 -0
- ksadk/harness/insights.py +210 -0
- ksadk/harness/lifecycle.py +578 -0
- ksadk/harness/lifecycle_conformance.py +233 -0
- ksadk/harness/lifecycle_local_plane.py +104 -0
- ksadk/harness/loop/__init__.py +44 -0
- ksadk/harness/loop/reason.py +699 -0
- ksadk/harness/loop/tools.py +907 -0
- ksadk/harness/managed_runtime.py +1044 -0
- ksadk/harness/mcp_disclosure_contract.py +85 -0
- ksadk/harness/mcp_matrix.py +474 -0
- ksadk/harness/mcp_result_policy.py +44 -0
- ksadk/harness/mcp_runtime.py +504 -0
- ksadk/harness/memory_consolidation.py +157 -0
- ksadk/harness/memory_loop.py +156 -0
- ksadk/harness/memory_runtime.py +541 -0
- ksadk/harness/metrics.py +90 -0
- ksadk/harness/micro_compact.py +69 -0
- ksadk/harness/model_adapters/__init__.py +1 -0
- ksadk/harness/model_adapters/native_mcp_media.py +100 -0
- ksadk/harness/model_adapters/native_mcp_result.py +36 -0
- ksadk/harness/model_adapters/native_openai.py +703 -0
- ksadk/harness/model_capability.py +468 -0
- ksadk/harness/model_provider.py +430 -0
- ksadk/harness/multimodal.py +57 -0
- ksadk/harness/native/__init__.py +1 -0
- ksadk/harness/native/activation_contract.py +25 -0
- ksadk/harness/native/activation_recovery.py +29 -0
- ksadk/harness/native/adapters.py +291 -0
- ksadk/harness/native/artifact_contract.py +14 -0
- ksadk/harness/native/artifact_partial_read_capacity.py +171 -0
- ksadk/harness/native/artifact_read_capacity.py +182 -0
- ksadk/harness/native/artifact_read_contract.py +27 -0
- ksadk/harness/native/artifact_runtime.py +408 -0
- ksadk/harness/native/artifact_search_capacity.py +206 -0
- ksadk/harness/native/artifact_search_contract.py +40 -0
- ksadk/harness/native/artifact_single_read_capacity.py +170 -0
- ksadk/harness/native/browser_contract.py +229 -0
- ksadk/harness/native/browser_recovery.py +183 -0
- ksadk/harness/native/capabilities.py +204 -0
- ksadk/harness/native/child_approval.py +60 -0
- ksadk/harness/native/child_budget.py +136 -0
- ksadk/harness/native/child_cancellation.py +80 -0
- ksadk/harness/native/child_input.py +98 -0
- ksadk/harness/native/child_input_runtime.py +59 -0
- ksadk/harness/native/child_interruption.py +81 -0
- ksadk/harness/native/child_lifecycle.py +246 -0
- ksadk/harness/native/child_lifecycle_contract.py +91 -0
- ksadk/harness/native/child_notifications.py +75 -0
- ksadk/harness/native/child_promotion.py +66 -0
- ksadk/harness/native/command_runtime.py +99 -0
- ksadk/harness/native/completion.py +302 -0
- ksadk/harness/native/completion_text.py +116 -0
- ksadk/harness/native/context.py +358 -0
- ksadk/harness/native/context_artifact_page.py +75 -0
- ksadk/harness/native/context_compaction_projection.py +131 -0
- ksadk/harness/native/context_contributions.py +320 -0
- ksadk/harness/native/context_contributor_contract.py +133 -0
- ksadk/harness/native/context_driver.py +820 -0
- ksadk/harness/native/context_fresh_tools.py +49 -0
- ksadk/harness/native/context_history.py +272 -0
- ksadk/harness/native/context_microcompact.py +61 -0
- ksadk/harness/native/context_partial_summary.py +58 -0
- ksadk/harness/native/context_partition.py +492 -0
- ksadk/harness/native/context_pruning.py +275 -0
- ksadk/harness/native/context_reduction.py +60 -0
- ksadk/harness/native/context_replay.py +148 -0
- ksadk/harness/native/context_request_projection.py +526 -0
- ksadk/harness/native/context_runtime.py +976 -0
- ksadk/harness/native/context_runtime_contracts.py +409 -0
- ksadk/harness/native/context_search.py +112 -0
- ksadk/harness/native/context_source_fragments.py +188 -0
- ksadk/harness/native/context_source_progress.py +119 -0
- ksadk/harness/native/context_summary_planning.py +788 -0
- ksadk/harness/native/context_summary_recovery.py +105 -0
- ksadk/harness/native/delivery_review.py +331 -0
- ksadk/harness/native/delivery_review_contract.py +54 -0
- ksadk/harness/native/effect_admission.py +82 -0
- ksadk/harness/native/effect_control.py +318 -0
- ksadk/harness/native/effect_receipts.py +729 -0
- ksadk/harness/native/effect_runtime.py +303 -0
- ksadk/harness/native/effective_configuration.py +28 -0
- ksadk/harness/native/engine.py +945 -0
- ksadk/harness/native/final_output.py +34 -0
- ksadk/harness/native/final_submission.py +58 -0
- ksadk/harness/native/finalization_workspace.py +96 -0
- ksadk/harness/native/fork.py +151 -0
- ksadk/harness/native/guidance.py +91 -0
- ksadk/harness/native/history.py +381 -0
- ksadk/harness/native/hook_executor.py +205 -0
- ksadk/harness/native/hook_runtime_contract.py +282 -0
- ksadk/harness/native/interaction_observation.py +117 -0
- ksadk/harness/native/lease.py +103 -0
- ksadk/harness/native/machine.py +974 -0
- ksadk/harness/native/mcp_result_governance.py +156 -0
- ksadk/harness/native/model.py +570 -0
- ksadk/harness/native/model_feedback.py +63 -0
- ksadk/harness/native/model_input.py +30 -0
- ksadk/harness/native/model_recovery_contract.py +35 -0
- ksadk/harness/native/model_replay.py +128 -0
- ksadk/harness/native/model_request.py +133 -0
- ksadk/harness/native/model_runtime.py +196 -0
- ksadk/harness/native/model_takeover.py +419 -0
- ksadk/harness/native/model_tool_batch.py +40 -0
- ksadk/harness/native/observed_progress.py +46 -0
- ksadk/harness/native/operator_guidance.py +201 -0
- ksadk/harness/native/output_contract.py +80 -0
- ksadk/harness/native/output_recovery.py +24 -0
- ksadk/harness/native/ports.py +749 -0
- ksadk/harness/native/precompact_hook_runtime.py +171 -0
- ksadk/harness/native/replay_context.py +149 -0
- ksadk/harness/native/request_media.py +112 -0
- ksadk/harness/native/restore_contract.py +16 -0
- ksadk/harness/native/run_clock.py +120 -0
- ksadk/harness/native/state.py +436 -0
- ksadk/harness/native/steering.py +170 -0
- ksadk/harness/native/stop_hook_runtime.py +151 -0
- ksadk/harness/native/studio_admission.py +29 -0
- ksadk/harness/native/subagents.py +86 -0
- ksadk/harness/native/task_policy.py +99 -0
- ksadk/harness/native/task_progress.py +86 -0
- ksadk/harness/native/terminal_verification.py +43 -0
- ksadk/harness/native/tool_argument_feedback.py +178 -0
- ksadk/harness/native/tool_budget.py +253 -0
- ksadk/harness/native/tool_error_streak.py +89 -0
- ksadk/harness/native/tool_hook_runtime.py +343 -0
- ksadk/harness/native/tool_input_validation.py +58 -0
- ksadk/harness/native/tool_interruption.py +96 -0
- ksadk/harness/native/tool_observation.py +259 -0
- ksadk/harness/native/tool_progress.py +223 -0
- ksadk/harness/native/tool_recovery.py +25 -0
- ksadk/harness/native/tool_scheduling.py +33 -0
- ksadk/harness/native/tool_schema.py +126 -0
- ksadk/harness/native/transitions.py +151 -0
- ksadk/harness/native/turn_control.py +98 -0
- ksadk/harness/native/user_document.py +98 -0
- ksadk/harness/native/user_document_capture.py +49 -0
- ksadk/harness/native/user_input.py +87 -0
- ksadk/harness/native/verification.py +432 -0
- ksadk/harness/native/verification_binding.py +40 -0
- ksadk/harness/native/verification_runtime.py +233 -0
- ksadk/harness/native/verification_workspace.py +67 -0
- ksadk/harness/native_driver_failure.py +123 -0
- ksadk/harness/native_facade.py +977 -0
- ksadk/harness/native_factory.py +244 -0
- ksadk/harness/native_projection.py +503 -0
- ksadk/harness/native_runtime.py +621 -0
- ksadk/harness/native_shadow.py +250 -0
- ksadk/harness/native_shadow_inputs.py +146 -0
- ksadk/harness/observability/__init__.py +29 -0
- ksadk/harness/observability/inspection.py +528 -0
- ksadk/harness/observability/metrics.py +167 -0
- ksadk/harness/observability/otel_exporter.py +218 -0
- ksadk/harness/progress_adapters/__init__.py +5 -0
- ksadk/harness/progress_adapters/native_todo.py +75 -0
- ksadk/harness/prompt_cache.py +132 -0
- ksadk/harness/public_activity.py +213 -0
- ksadk/harness/public_output.py +30 -0
- ksadk/harness/public_summary.py +36 -0
- ksadk/harness/readiness.py +249 -0
- ksadk/harness/reasoner.py +779 -17
- ksadk/harness/release_readiness.py +140 -0
- ksadk/harness/resource_ref.py +69 -0
- ksadk/harness/rpc.py +200 -0
- ksadk/harness/run_control.py +773 -0
- ksadk/harness/runtime.py +157 -113
- ksadk/harness/runtime_capabilities.py +228 -0
- ksadk/harness/runtime_server.py +514 -0
- ksadk/harness/sandbox_adapters.py +739 -0
- ksadk/harness/sandbox_backend.py +733 -0
- ksadk/harness/sandbox_conformance.py +288 -0
- ksadk/harness/sandbox_lease.py +77 -0
- ksadk/harness/sandbox_lease_conformance.py +259 -0
- ksadk/harness/sandbox_matrix.py +170 -0
- ksadk/harness/sandbox_reconciliation.py +140 -0
- ksadk/harness/sandbox_recovery.py +586 -0
- ksadk/harness/sandbox_recovery_conformance.py +246 -0
- ksadk/harness/sdk.py +153 -0
- ksadk/harness/session_store.py +589 -0
- ksadk/harness/skill_adapters/__init__.py +1 -0
- ksadk/harness/skill_adapters/native_bundle.py +148 -0
- ksadk/harness/skill_adapters/native_disclosure.py +306 -0
- ksadk/harness/skill_composition.py +212 -0
- ksadk/harness/skill_feedback.py +123 -0
- ksadk/harness/skill_intent.py +47 -0
- ksadk/harness/skill_matrix.py +233 -0
- ksadk/harness/skill_runtime.py +317 -0
- ksadk/harness/spec.py +342 -0
- ksadk/harness/state.py +155 -0
- ksadk/harness/stores/__init__.py +14 -0
- ksadk/harness/stores/native_command_settlement.py +65 -0
- ksadk/harness/stores/native_contract.py +311 -0
- ksadk/harness/stores/native_memory.py +326 -0
- ksadk/harness/stores/native_memory_archive.py +70 -0
- ksadk/harness/stores/native_migration_cli.py +91 -0
- ksadk/harness/stores/native_outbox.py +56 -0
- ksadk/harness/stores/native_sqlite.py +782 -0
- ksadk/harness/stores/native_sqlite_audit.py +29 -0
- ksadk/harness/stores/native_sqlite_migration.py +252 -0
- ksadk/harness/strategies.py +149 -0
- ksadk/harness/subagent.py +263 -0
- ksadk/harness/subagent_adapters/checkpoint_recovery.py +120 -0
- ksadk/harness/subagent_adapters/native_background_child.py +803 -0
- ksadk/harness/subagent_adapters/native_child.py +819 -0
- ksadk/harness/subagent_adapters/native_child_approval.py +128 -0
- ksadk/harness/subagent_adapters/native_child_continuation.py +51 -0
- ksadk/harness/subagent_adapters/native_child_result.py +392 -0
- ksadk/harness/subagent_adapters/prepared_recovery.py +120 -0
- ksadk/harness/tool_display.py +75 -0
- ksadk/harness/tool_display_contract.py +134 -0
- ksadk/harness/tool_output.py +141 -0
- ksadk/harness/tool_policy.py +134 -0
- ksadk/harness/tool_receipts.py +96 -0
- ksadk/harness/tool_reliability.py +81 -0
- ksadk/harness/tool_result_policy.py +216 -0
- ksadk/harness/tools.py +52 -0
- ksadk/harness/user_errors.py +69 -0
- ksadk/harness/working_context.py +81 -0
- ksadk/harness/working_context_patch.py +99 -0
- ksadk/harness/workspace_adapters/__init__.py +6 -0
- ksadk/harness/workspace_adapters/native_directory.py +180 -0
- ksadk/harness/workspace_adapters/native_git.py +208 -0
- ksadk/harness/workspace_adapters/native_isolation.py +350 -0
- ksadk/identity/resolver.py +18 -0
- ksadk/interaction/__init__.py +1 -1
- ksadk/interaction/ledger.py +1 -1
- ksadk/interaction/provider.py +1 -1
- ksadk/interaction/providers/__init__.py +3 -1
- ksadk/interaction/providers/adk.py +1 -1
- ksadk/interaction/providers/codex.py +1 -1
- ksadk/interaction/providers/harness.py +50 -0
- ksadk/interaction/providers/langgraph.py +9 -2
- ksadk/kernel/__init__.py +10 -0
- ksadk/kernel/approval_presentation.py +91 -0
- ksadk/kernel/authorization.py +1 -1
- ksadk/kernel/bootstrap.py +220 -252
- ksadk/kernel/contract_fingerprints.py +45 -1
- ksadk/kernel/contracts.py +30 -0
- ksadk/kernel/control.py +180 -29
- ksadk/kernel/execution_grants.py +428 -0
- ksadk/kernel/execution_grants_memory.py +252 -0
- ksadk/kernel/execution_grants_postgres.py +395 -0
- ksadk/kernel/execution_grants_sqlite.py +435 -0
- ksadk/kernel/execution_host_ingress.py +1071 -0
- ksadk/kernel/history_projection.py +120 -0
- ksadk/kernel/ingress.py +139 -226
- ksadk/kernel/ingress_bootstrap.py +268 -0
- ksadk/kernel/legacy_history.py +179 -0
- ksadk/kernel/mapping.py +1 -1
- ksadk/kernel/memory_store.py +61 -58
- ksadk/kernel/memory_store_projection.py +63 -0
- ksadk/kernel/postgres_event_log.py +181 -0
- ksadk/kernel/postgres_store.py +230 -231
- ksadk/kernel/readiness.py +125 -0
- ksadk/kernel/recovery.py +166 -16
- ksadk/kernel/recovery_projection.py +124 -0
- ksadk/kernel/runtime_checkpoint.py +34 -0
- ksadk/kernel/sql/001_agent_kernel.sql +28 -0
- ksadk/kernel/sqlite_schema.py +100 -0
- ksadk/kernel/sqlite_store.py +410 -200
- ksadk/kernel/state.py +2 -3
- ksadk/kernel/store.py +104 -5
- ksadk/kernel/teams_artifact_tools.py +392 -0
- ksadk/kernel/teams_effects.py +851 -0
- ksadk/kernel/teams_execution_context.py +408 -0
- ksadk/kernel/teams_host_http.py +237 -0
- ksadk/kernel/teams_material_transport.py +245 -0
- ksadk/kernel/teams_materials.py +318 -0
- ksadk/kernel/teams_runtime_bootstrap.py +604 -0
- ksadk/kernel/teams_tool_runtime.py +273 -0
- ksadk/kernel/teams_workspace.py +259 -0
- ksadk/kernel/worker.py +369 -340
- ksadk/kernel/worker_controls.py +454 -0
- ksadk/kernel/worker_history.py +72 -0
- ksadk/kernel/worker_identity.py +74 -0
- ksadk/kernel/worker_interactions.py +197 -0
- ksadk/knowledge_base/client.py +15 -1
- ksadk/managed_runtime.py +29 -2
- ksadk/memory/adk/backends/http_ltm_backend.py +2 -1
- ksadk/memory/adk/backends/sdk_ltm_backend.py +15 -1
- ksadk/memory/adk/bounded_database_session_service.py +232 -0
- ksadk/memory/adk/resilient_session_service.py +115 -15
- ksadk/memory/adk/short_term_memory.py +8 -9
- ksadk/memory/coordinator.py +22 -6
- ksadk/memory/extraction.py +159 -7
- ksadk/memory/models.py +77 -1
- ksadk/memory/provider_resolver.py +5 -0
- ksadk/memory/providers/local_candidates.py +183 -0
- ksadk/memory/providers/local_postgres.py +200 -0
- ksadk/memory/providers/local_sqlite.py +280 -67
- ksadk/memory/resolved_policy.py +12 -7
- ksadk/memory/retrieval.py +189 -0
- ksadk/memory/store_adapter.py +150 -0
- ksadk/model_gateway/__init__.py +29 -0
- ksadk/model_gateway/client.py +253 -0
- ksadk/model_gateway/events.py +113 -0
- ksadk/model_gateway/matrix.py +297 -0
- ksadk/model_policy.py +2 -2
- ksadk/model_proxy/compat.py +670 -0
- ksadk/model_proxy/detect.py +254 -0
- ksadk/model_proxy/server.py +14 -9
- ksadk/model_proxy/transform.py +72 -6
- ksadk/plugins/__init__.py +1 -1
- ksadk/plugins/bridges/dsh.py +29 -20
- ksadk/plugins/bridges/dsh_profile_manifest.py +36 -0
- ksadk/plugins/builtin_resources.py +199 -0
- ksadk/plugins/builtins.py +417 -185
- ksadk/plugins/companion_artifacts.py +83 -0
- ksadk/plugins/companions.py +368 -0
- ksadk/plugins/context_contributor.py +15 -4
- ksadk/plugins/contracts.py +1 -1
- ksadk/plugins/dsh_channels.py +156 -0
- ksadk/plugins/dsh_dependency_policy.py +41 -0
- ksadk/plugins/dsh_home.py +140 -0
- ksadk/plugins/dsh_teams.py +105 -0
- ksadk/plugins/dsh_toolchain.py +92 -21
- ksadk/plugins/execution_host.py +107 -0
- ksadk/plugins/host.py +73 -5
- ksadk/plugins/kernel_adapter.py +44 -1
- ksadk/plugins/memory_provider.py +180 -0
- ksadk/plugins/providers/_process_group.py +50 -0
- ksadk/plugins/providers/bundles/dsh-channels-client/client.js +19 -0
- ksadk/plugins/providers/bundles/dsh-channels-client/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/dsh-channels-client/index.mjs +7 -0
- ksadk/plugins/providers/bundles/dsh-channels-client/package.json +12 -0
- ksadk/plugins/providers/bundles/dsh-knowledge/index.mjs +24 -2
- ksadk/plugins/providers/bundles/dsh-knowledge/package.json +1 -1
- ksadk/plugins/providers/bundles/dsh-memory/package.json +1 -1
- ksadk/plugins/providers/bundles/dsh-skill-center/package.json +1 -1
- ksadk/plugins/providers/bundles/dsh-teams-client/client.js +29 -0
- ksadk/plugins/providers/bundles/dsh-teams-client/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/dsh-teams-client/index.mjs +11 -0
- ksadk/plugins/providers/bundles/dsh-teams-client/package.json +35 -0
- ksadk/plugins/providers/bundles/dsh-teams-leader-policy/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/dsh-teams-leader-policy/index.mjs +13 -0
- ksadk/plugins/providers/bundles/dsh-teams-leader-policy/package.json +27 -0
- ksadk/plugins/providers/bundles/dsh-teams-profile/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/dsh-teams-profile/index.mjs +12 -0
- ksadk/plugins/providers/bundles/dsh-teams-profile/package.json +26 -0
- ksadk/plugins/providers/bundles/dsh-teams-router/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/dsh-teams-router/index.mjs +16 -0
- ksadk/plugins/providers/bundles/dsh-teams-router/package.json +27 -0
- ksadk/plugins/providers/bundles/dsh-teams-tasks/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/dsh-teams-tasks/index.mjs +16 -0
- ksadk/plugins/providers/bundles/dsh-teams-tasks/package.json +27 -0
- ksadk/plugins/providers/bundles/dsh-teams-tools/cordis.patch.yml +3 -0
- ksadk/plugins/providers/bundles/dsh-teams-tools/index.mjs +25 -0
- ksadk/plugins/providers/bundles/dsh-teams-tools/package.json +29 -0
- ksadk/plugins/providers/bundles/dsh-teams-tools/tools.json +235 -0
- ksadk/plugins/providers/bundles/ksadk-codex/index.mjs +1 -1
- ksadk/plugins/providers/bundles/ksadk-dsh-capability-host/index.mjs +51 -1
- ksadk/plugins/providers/bundles/ksadk-dsh-capability-host/package.json +2 -2
- ksadk/plugins/providers/bundles/ksadk-dsh-companion-host/client.mjs +133 -0
- ksadk/plugins/providers/bundles/ksadk-dsh-companion-host/index.mjs +14 -0
- ksadk/plugins/providers/bundles/ksadk-dsh-studio/client.js +27 -4
- ksadk/plugins/providers/bundles/ksadk-dsh-studio/index.mjs +9 -5
- ksadk/plugins/providers/bundles/ksadk-harness/index.mjs +1 -1
- ksadk/plugins/providers/codex.py +52 -40
- ksadk/plugins/providers/codex_native.py +20 -0
- ksadk/plugins/providers/codex_skills.py +46 -0
- ksadk/plugins/providers/dsh_capabilities.py +49 -143
- ksadk/plugins/providers/dsh_connector_lease.py +225 -0
- ksadk/plugins/providers/dsh_mcp.py +163 -1
- ksadk/plugins/providers/harness.py +413 -52
- ksadk/plugins/providers/harness_context.py +61 -0
- ksadk/plugins/providers/harness_contributions.py +241 -0
- ksadk/plugins/providers/harness_delegation.py +670 -0
- ksadk/plugins/providers/harness_hooks.py +121 -0
- ksadk/plugins/providers/harness_managed.py +550 -0
- ksadk/plugins/providers/harness_tools.py +455 -0
- ksadk/plugins/providers/legacy_catalog.py +1 -1
- ksadk/plugins/providers/mcp_projection.py +100 -3
- ksadk/plugins/providers/platform_resources.py +16 -6
- ksadk/plugins/providers/shipped_dsh.py +4 -1
- ksadk/plugins/subagent_providers/codex.py +28 -6
- ksadk/plugins/teams/__init__.py +5 -0
- ksadk/plugins/teams/api.py +405 -0
- ksadk/plugins/teams/application.py +540 -0
- ksadk/plugins/teams/artifacts.py +60 -0
- ksadk/plugins/teams/build_artifacts.py +460 -0
- ksadk/plugins/teams/cloud_contract_fingerprints.py +22 -0
- ksadk/plugins/teams/cloud_contracts.py +928 -0
- ksadk/plugins/teams/cloud_permits.py +250 -0
- ksadk/plugins/teams/context.py +96 -0
- ksadk/plugins/teams/contracts.py +136 -0
- ksadk/plugins/teams/domain.py +1480 -0
- ksadk/plugins/teams/domain_values.py +24 -0
- ksadk/plugins/teams/effect_contracts.py +255 -0
- ksadk/plugins/teams/errors.py +13 -0
- ksadk/plugins/teams/execution_projection.py +255 -0
- ksadk/plugins/teams/leader_decisions.py +106 -0
- ksadk/plugins/teams/migration.py +311 -0
- ksadk/plugins/teams/migration_cli.py +322 -0
- ksadk/plugins/teams/postgres_store.py +352 -0
- ksadk/plugins/teams/release_contracts.py +60 -0
- ksadk/plugins/teams/runtime.py +853 -0
- ksadk/plugins/teams/source_projection.py +84 -0
- ksadk/plugins/teams/store.py +519 -0
- ksadk/plugins/teams/task_decisions.py +436 -0
- ksadk/plugins/teams/tool_service.py +152 -0
- ksadk/plugins/teams/transport.py +96 -0
- ksadk/plugins/teams/workspaces.py +255 -0
- ksadk/resource_runtime/skill_runtime.py +3 -0
- ksadk/resource_runtime/socket_server.py +11 -0
- ksadk/resource_runtime/worker.py +28 -4
- ksadk/runners/_adk_a2a_stream.py +122 -0
- ksadk/runners/_adk_canonical_stream.py +474 -0
- ksadk/runners/_langgraph_canonical_streams.py +527 -0
- ksadk/runners/_langgraph_runner_streams.py +83 -401
- ksadk/runners/_langgraph_state_reader.py +106 -0
- ksadk/runners/_langgraph_tool_projection.py +86 -0
- ksadk/runners/_langgraph_tools_transformer.py +27 -0
- ksadk/runners/_session_identity.py +285 -0
- ksadk/runners/adk_runner.py +220 -350
- ksadk/runners/base_runner.py +6 -5
- ksadk/runners/langgraph_runner.py +208 -195
- ksadk/runners/patch_langchain.py +47 -0
- ksadk/runners/usage_accumulator.py +11 -3
- ksadk/runners/utils/loader.py +28 -14
- ksadk/runtime/_runner_adapter/a2ui_surface.py +145 -0
- ksadk/runtime/_runner_adapter/gateway_approvals.py +93 -0
- ksadk/runtime/_runner_adapter/stream_mapping.py +134 -129
- ksadk/runtime/adapter.py +40 -3
- ksadk/runtime/conversation_execution.py +29 -3
- ksadk/runtime/executor.py +105 -7
- ksadk/runtime/factory.py +45 -3
- ksadk/runtime/framework_adapters.py +184 -10
- ksadk/runtime/hosted_finalizer.py +4 -6
- ksadk/runtime/launch.py +2 -0
- ksadk/runtime/preprocessing.py +11 -3
- ksadk/runtime/runner_adapter.py +160 -21
- ksadk/runtime/skill_eval_result.py +36 -0
- ksadk/runtime/timing.py +46 -0
- ksadk/runtime/usage.py +49 -18
- ksadk/runtime_context.py +105 -1
- ksadk/sandbox/backends/e2b.py +272 -24
- ksadk/sandbox/backends/local_process.py +73 -39
- ksadk/sandbox/base.py +62 -1
- ksadk/sandbox/diagnostics.py +82 -0
- ksadk/sandbox/e2b_connection.py +9 -1
- ksadk/sandbox/local_controls.py +776 -0
- ksadk/sandbox/local_launcher.py +124 -0
- ksadk/scheduler/dispatcher.py +1 -1
- ksadk/server/factory.py +178 -25
- ksadk/server/history_deadline.py +158 -0
- ksadk/server/history_page_cache.py +129 -0
- ksadk/server/invocation_identity.py +118 -0
- ksadk/server/routes/checkpoint_audit.py +88 -0
- ksadk/server/routes/checkpoint_listing.py +294 -0
- ksadk/server/routes/checkpoint_resolution.py +73 -20
- ksadk/server/routes/common.py +32 -3
- ksadk/server/routes/control.py +112 -9
- ksadk/server/routes/dependencies.py +2 -1
- ksadk/server/routes/display_projection.py +154 -0
- ksadk/server/routes/kernel_ingress.py +26 -3
- ksadk/server/routes/misc.py +331 -33
- ksadk/server/routes/models.py +28 -4
- ksadk/server/routes/openai_compat.py +82 -13
- ksadk/server/routes/projection.py +159 -216
- ksadk/server/routes/run.py +112 -37
- ksadk/server/routes/sessions.py +493 -358
- ksadk/server/routes/streaming.py +68 -15
- ksadk/server/routes/tool_receipt_claim_routes.py +107 -0
- ksadk/server/routes/workspace.py +135 -23
- ksadk/server/static/assets/{ArtifactsPanel-CGbWlMNh.js → ArtifactsPanel-CILaW-fC.js} +1 -1
- ksadk/server/static/assets/{CodeBlock-b4C7upmO.js → CodeBlock-BVrNVtJC.js} +10 -7
- ksadk/server/static/assets/MathMessageMarkdown-Dp7c4wWD.js +8 -0
- ksadk/server/static/assets/{MermaidBlock-BEBN4Ksz.js → MermaidBlock-Dxop8Rd7.js} +9 -9
- ksadk/server/static/assets/{NativeTerminalPanel-zvL-WpAn.js → NativeTerminalPanel-Cjjz8B30.js} +1 -1
- ksadk/server/static/assets/{abnfDiagram-VCTEODGH-BYafW7Za.js → abnfDiagram-VCTEODGH-ckWDmOzw.js} +1 -1
- ksadk/server/static/assets/{arc-bTohInsS.js → arc-DqxJede-.js} +1 -1
- ksadk/server/static/assets/architecture-7GRP2DOG-pn_DuLy6.js +1 -0
- ksadk/server/static/assets/{architectureDiagram-5GKGNRK7-Ch5Lbzsi.js → architectureDiagram-5GKGNRK7-SEQA4bVi.js} +1 -1
- ksadk/server/static/assets/{blockDiagram-NRAW4CY4-kqF4MDOG.js → blockDiagram-NRAW4CY4-pM0B-6NJ.js} +1 -1
- ksadk/server/static/assets/{c4Diagram-UCG6FXSJ-8uSRIODv.js → c4Diagram-UCG6FXSJ-m2tuCNVq.js} +1 -1
- ksadk/server/static/assets/channel-fySsnkft.js +1 -0
- ksadk/server/static/assets/chunk-2Q5K7J3B-BheGxKRQ.js +1 -0
- ksadk/server/static/assets/{chunk-5VM5RSS4-DkhehZDr.js → chunk-5VM5RSS4-GJHDr9Ww.js} +1 -1
- ksadk/server/static/assets/{chunk-F27PBJKO-BdXhtPe9.js → chunk-F27PBJKO-CbAN45AV.js} +1 -1
- ksadk/server/static/assets/{chunk-G27WJ6UU-DPTpvpwi.js → chunk-G27WJ6UU-D5ngbbzF.js} +1 -1
- ksadk/server/static/assets/{chunk-JWPE2WC7-Pqoh38el.js → chunk-JWPE2WC7-Cc5IwuYC.js} +1 -1
- ksadk/server/static/assets/{chunk-LCL6LL3I-DeU00qC-.js → chunk-LCL6LL3I-CprgDDdJ.js} +1 -1
- ksadk/server/static/assets/{chunk-POPQ4Y6H-CzxScFkr.js → chunk-POPQ4Y6H-CDZhSy82.js} +1 -1
- ksadk/server/static/assets/{chunk-RHFEMEQ7-DpqMx-ux.js → chunk-RHFEMEQ7-CZchUNro.js} +1 -1
- ksadk/server/static/assets/{chunk-SVP7TREG-C6t0SypQ.js → chunk-SVP7TREG-BVruqk6T.js} +1 -1
- ksadk/server/static/assets/{chunk-XXDRQBXY-DVaeafZk.js → chunk-XXDRQBXY-4ld9SxPN.js} +1 -1
- ksadk/server/static/assets/classDiagram-DTDB5LWJ-DSfLtNYm.js +1 -0
- ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-DSfLtNYm.js +1 -0
- ksadk/server/static/assets/{cose-bilkent-JH36ORCC-Bf9D3tzJ.js → cose-bilkent-JH36ORCC-Dmyyk_Vp.js} +1 -1
- ksadk/server/static/assets/cynefin-OW5HDTMX-PFWIGtvA.js +1 -0
- ksadk/server/static/assets/{cynefinDiagram-5FMLGOSQ-B9cY3iTz.js → cynefinDiagram-5FMLGOSQ-BIyq3EJb.js} +1 -1
- ksadk/server/static/assets/{dagre-3AP2YEHR-CSwlx3CB.js → dagre-3AP2YEHR-eBw08Qdk.js} +1 -1
- ksadk/server/static/assets/{dagre-UBpgon5q.js → dagre-DfvnEf45.js} +1 -1
- ksadk/server/static/assets/{diagram-S7CK7UJ4-C9akhN9a.js → diagram-S7CK7UJ4-CUSrE3Mg.js} +1 -1
- ksadk/server/static/assets/{diagram-UQ7AKVKN-CgedhTPI.js → diagram-UQ7AKVKN-Dq5Zw2gy.js} +1 -1
- ksadk/server/static/assets/{diagram-VSXAHHWV-Cjucz5BY.js → diagram-VSXAHHWV-DyUHfNwT.js} +1 -1
- ksadk/server/static/assets/{diagram-VX7I27RA-Dd4Vleyn.js → diagram-VX7I27RA-CREmM89O.js} +1 -1
- ksadk/server/static/assets/{diagram-Z3DM3KII-CZ2jGtYW.js → diagram-Z3DM3KII-Dw9zhHUK.js} +1 -1
- ksadk/server/static/assets/{dist-Bj730Ib4.js → dist-0uJ4tdx6.js} +1 -1
- ksadk/server/static/assets/{dist-BnaTiyzN.js → dist-AEv-vXf-.js} +1 -1
- ksadk/server/static/assets/{dist-BgV_owcB.js → dist-B49GzZDb.js} +1 -1
- ksadk/server/static/assets/{dist-D_ctSS2i.js → dist-B8Myiet9.js} +1 -1
- ksadk/server/static/assets/{dist-ufY3F5p6.js → dist-BBsYarrI.js} +2 -2
- ksadk/server/static/assets/{dist-ClYUxfeK.js → dist-BQMrUwdI.js} +1 -1
- ksadk/server/static/assets/{dist-CFzsn6BG.js → dist-BX_-FENW.js} +1 -1
- ksadk/server/static/assets/{dist-B3FR1SKK.js → dist-CD4L6ymH.js} +1 -1
- ksadk/server/static/assets/{dist-Cf125P_m.js → dist-CHpW1BHj.js} +1 -1
- ksadk/server/static/assets/{dist-DVCOWtha.js → dist-CVPau3tn.js} +1 -1
- ksadk/server/static/assets/{dist-DFD6Qa6T.js → dist-C_qdxcSH.js} +1 -1
- ksadk/server/static/assets/{dist-BIr4DSRq.js → dist-Co68Apq4.js} +1 -1
- ksadk/server/static/assets/{dist-DiixqL0J.js → dist-Cp2PaOMY.js} +1 -1
- ksadk/server/static/assets/{dist-fBO2v5Kt.js → dist-CuR5Bgme.js} +1 -1
- ksadk/server/static/assets/{dist-DoVSwoDb.js → dist-DDSyLEOX.js} +1 -1
- ksadk/server/static/assets/{dist-C5_ZAZsG.js → dist-DUxO0R99.js} +1 -1
- ksadk/server/static/assets/{dist-C1NMAY5a.js → dist-QzbGrED_.js} +1 -1
- ksadk/server/static/assets/dist-bupkyEBZ.js +1 -0
- ksadk/server/static/assets/{dist-DvFK7p5S.js → dist-cxTGfdqx.js} +1 -1
- ksadk/server/static/assets/{dist-CB9iJrvg.js → dist-hL5p1til.js} +1 -1
- ksadk/server/static/assets/{dist-btbmmX5O.js → dist-lOy23y43.js} +1 -1
- ksadk/server/static/assets/{dist-QfkEHOlb.js → dist-pq-nD33R.js} +1 -1
- ksadk/server/static/assets/{ebnfDiagram-PWID7BFC-Ci8NqqZu.js → ebnfDiagram-PWID7BFC-BS5LdNoR.js} +1 -1
- ksadk/server/static/assets/{erDiagram-SSCWMZ5O-CCIOhLZG.js → erDiagram-SSCWMZ5O-Bv1QtiY-.js} +1 -1
- ksadk/server/static/assets/eventmodeling-NTZA5JFV-DsM4upQX.js +1 -0
- ksadk/server/static/assets/flowDiagram-A5DVABFB-D5NA16k9.js +1 -0
- ksadk/server/static/assets/{ganttDiagram-EL5Y4UJY-Cp_r9Hb6.js → ganttDiagram-EL5Y4UJY-musi-e0G.js} +1 -1
- ksadk/server/static/assets/gitGraph-4MIJSDKK-DUCVz-ih.js +1 -0
- ksadk/server/static/assets/{gitGraphDiagram-WWUBYQGX-Dlhc8Wr8.js → gitGraphDiagram-WWUBYQGX-DH37EGkG.js} +1 -1
- ksadk/server/static/assets/index-CZdkAvsv.js +244 -0
- ksadk/server/static/assets/index-CvXdHgF3.css +2 -0
- ksadk/server/static/assets/info-A6RAGUB7-rw0ZNHZp.js +1 -0
- ksadk/server/static/assets/{infoDiagram-RXCK75RN-CO0rRTRA.js → infoDiagram-RXCK75RN-C1kEtv2y.js} +1 -1
- ksadk/server/static/assets/{ishikawaDiagram-5VMMS53U-CHiaedSS.js → ishikawaDiagram-5VMMS53U-BCNPyMka.js} +1 -1
- ksadk/server/static/assets/{journeyDiagram-EYS64GPL-CHcCIB09.js → journeyDiagram-EYS64GPL--cDbKnjP.js} +1 -1
- ksadk/server/static/assets/{kanban-definition-3QL26DDD-CJ_ebBZf.js → kanban-definition-3QL26DDD-R1rAVw2f.js} +1 -1
- ksadk/server/static/assets/{katex-B4weu383.js → katex-BT-X-V7U.js} +1 -1
- ksadk/server/static/assets/{linear-V-MgYHW1.js → linear-DTR631iG.js} +1 -1
- ksadk/server/static/assets/{mermaid-parser.core-CBijEX_c.js → mermaid-parser.core-DfFzJhX8.js} +2 -2
- ksadk/server/static/assets/{mindmap-definition-FBJOCRG2-Bc-pQvpp.js → mindmap-definition-FBJOCRG2-D4by_rmk.js} +1 -1
- ksadk/server/static/assets/packet-AYTQ26CC-Ca4Hvki9.js +1 -0
- ksadk/server/static/assets/{pegDiagram-XKGWAZYB-D7genGLl.js → pegDiagram-XKGWAZYB-xWXPEh0c.js} +1 -1
- ksadk/server/static/assets/pie-WAS4IAKB-hwiz6fbw.js +1 -0
- ksadk/server/static/assets/{pieDiagram-E7YTZNPT-Bxj32zwD.js → pieDiagram-E7YTZNPT-BudBDlCI.js} +1 -1
- ksadk/server/static/assets/{quadrantDiagram-AXDQQJYC-BURv2mGh.js → quadrantDiagram-AXDQQJYC-DXZ0PCJF.js} +1 -1
- ksadk/server/static/assets/radar-RG4KPBEZ-BR1oZXLM.js +1 -0
- ksadk/server/static/assets/railroad-74A4TZTK-DfgEISfX.js +1 -0
- ksadk/server/static/assets/railroad-abnf-HS5TGJTU-CWOC3rdk.js +1 -0
- ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-CiRvHCLY.js +1 -0
- ksadk/server/static/assets/railroad-peg-WCYAUIDC-DSDDyJ2f.js +1 -0
- ksadk/server/static/assets/{railroadDiagram-O6MQD6OU-CCsh3avW.js → railroadDiagram-O6MQD6OU-BdAYRYB8.js} +1 -1
- ksadk/server/static/assets/{requirementDiagram-EFPCY7ZU-_FRRBKEd.js → requirementDiagram-EFPCY7ZU-CBBmaL70.js} +1 -1
- ksadk/server/static/assets/{sankeyDiagram-P5KCCOFB-C-gr2o9M.js → sankeyDiagram-P5KCCOFB-Doc3rdwp.js} +1 -1
- ksadk/server/static/assets/{sequenceDiagram-WJ2MYXX4-D97NmfKg.js → sequenceDiagram-WJ2MYXX4-0J8R8hWW.js} +1 -1
- ksadk/server/static/assets/{sizeCapture-X5ZJPWSS-DjrPC5FX.js → sizeCapture-X5ZJPWSS-CfFLlWsK.js} +1 -1
- ksadk/server/static/assets/{stateDiagram-HBIQ2CUA-DzMNg1hh.js → stateDiagram-HBIQ2CUA-jBwkjtTy.js} +1 -1
- ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-Cw1fPb5w.js +1 -0
- ksadk/server/static/assets/{swimlanes-XN3QIQJK-Cssf_CsU.js → swimlanes-XN3QIQJK-B0WxTQf8.js} +1 -1
- ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-Cc3JMBAF.js +8 -0
- ksadk/server/static/assets/{timeline-definition-24CTP7MA-BWTHzyS1.js → timeline-definition-24CTP7MA-BfCT_75A.js} +1 -1
- ksadk/server/static/assets/treeView-Q6P3EWNA-C05SJHxo.js +1 -0
- ksadk/server/static/assets/treemap-WGGIJYW6-BqNLAhc3.js +1 -0
- ksadk/server/static/assets/{vennDiagram-4TSXK5OY-BSKgJsmR.js → vennDiagram-4TSXK5OY-9gX1VMDC.js} +1 -1
- ksadk/server/static/assets/wardley-WFR3VGLG-DokNktqB.js +1 -0
- ksadk/server/static/assets/{wardleyDiagram-VM6X3IG4-eflHNoaZ.js → wardleyDiagram-VM6X3IG4-sEs_BP6O.js} +1 -1
- ksadk/server/static/assets/{xychartDiagram-S5SC5T6Z-eYqrQ-v4.js → xychartDiagram-S5SC5T6Z-Ll4jvUN9.js} +1 -1
- ksadk/server/static/index.html +2 -2
- ksadk/server/terminal_sessions.py +34 -2
- ksadk/sessions/__init__.py +8 -1
- ksadk/sessions/_checkpoint_resume_audit.py +44 -0
- ksadk/sessions/_in_memory_query.py +112 -0
- ksadk/sessions/_local_service_sync.py +157 -156
- ksadk/sessions/_local_state_sync.py +152 -0
- ksadk/sessions/_local_tables.py +2 -0
- ksadk/sessions/_postgres_checkpoint_read.py +323 -0
- ksadk/sessions/_postgres_checkpoint_stats.py +330 -0
- ksadk/sessions/_postgres_history_read.py +678 -0
- ksadk/sessions/_postgres_schema.py +44 -2
- ksadk/sessions/_postgres_tables.py +2 -0
- ksadk/sessions/_postgres_tool_receipts.py +169 -0
- ksadk/sessions/_resilient_read.py +630 -0
- ksadk/sessions/_tool_receipt_sync.py +119 -0
- ksadk/sessions/agent_identity.py +16 -0
- ksadk/sessions/base.py +278 -0
- ksadk/sessions/db_budget.py +332 -0
- ksadk/sessions/errors.py +12 -0
- ksadk/sessions/history_budget.py +36 -0
- ksadk/sessions/history_identity.py +27 -0
- ksadk/sessions/in_memory.py +376 -113
- ksadk/sessions/invocation_identity.py +235 -0
- ksadk/sessions/local_service.py +345 -0
- ksadk/sessions/postgres_service.py +515 -549
- ksadk/sessions/resilience.py +21 -14
- ksadk/sessions/resilient.py +611 -504
- ksadk/skills/__init__.py +26 -0
- ksadk/skills/events.py +464 -0
- ksadk/skills/loader.py +44 -2
- ksadk/skills/manifest_cache.py +263 -0
- ksadk/skills/mcp_server/__init__.py +0 -0
- ksadk/skills/mcp_server/_registry.py +33 -0
- ksadk/skills/mcp_server/_routing.py +26 -0
- ksadk/skills/mcp_server/_workspace_files.py +160 -0
- ksadk/skills/mcp_server/register.py +415 -0
- ksadk/skills/mcp_server/server.py +218 -0
- ksadk/skills/models.py +11 -0
- ksadk/skills/observability.py +132 -0
- ksadk/skills/package_store.py +75 -2
- ksadk/skills/runtime/agent.py +38 -5
- ksadk/skills/runtime/artifacts.py +87 -0
- ksadk/skills/runtime/backends/e2b.py +400 -56
- ksadk/skills/runtime/backends/local.py +621 -119
- ksadk/skills/runtime/base.py +78 -7
- ksadk/skills/runtime/executor.py +266 -1
- ksadk/skills/runtime/loader.py +108 -3
- ksadk/skills/runtime/request.py +18 -0
- ksadk/skills/service_client.py +6 -1
- ksadk/skills/tool_defs.py +108 -3
- ksadk/studio/a2a_bindings.py +407 -0
- ksadk/studio/a2a_orchestration.py +178 -0
- ksadk/studio/active_run_output.py +92 -0
- ksadk/studio/activity_records.py +542 -0
- ksadk/studio/agent_configuration.py +173 -0
- ksadk/studio/api.py +784 -419
- ksadk/studio/api_a2a_routes.py +93 -0
- ksadk/studio/api_catalog_routes.py +13 -0
- ksadk/studio/api_cloud_chat_routes.py +382 -0
- ksadk/studio/api_contracts.py +31 -2
- ksadk/studio/api_plugin_routes.py +39 -12
- ksadk/studio/api_resource_connections.py +79 -41
- ksadk/studio/attachment_store.py +14 -2
- ksadk/studio/authoring.py +12 -6
- ksadk/studio/authoring_coordinator.py +4 -2
- ksadk/studio/build_resolution.py +117 -0
- ksadk/studio/build_singleflight.py +37 -0
- ksadk/studio/builder.py +32 -2
- ksadk/studio/capabilities.py +31 -9
- ksadk/studio/channel_connections.py +333 -0
- ksadk/studio/cloud.py +113 -387
- ksadk/studio/cloud_gateways.py +424 -0
- ksadk/studio/cloud_shared_web.py +82 -2
- ksadk/studio/codex_agent_service.py +19 -2
- ksadk/studio/codex_builder.py +1 -1
- ksadk/studio/codex_manifest.py +5 -1
- ksadk/studio/codex_provider_build.py +65 -0
- ksadk/studio/codex_run.py +6 -1
- ksadk/studio/compiler.py +112 -6
- ksadk/studio/configuration.py +182 -0
- ksadk/studio/contract_base.py +19 -0
- ksadk/studio/contracts.py +260 -202
- ksadk/studio/dsh_application.py +183 -27
- ksadk/studio/dsh_capability_service.py +302 -189
- ksadk/studio/dsh_lifecycle.py +58 -0
- ksadk/studio/dsh_mcp_transport.py +191 -0
- ksadk/studio/dsh_provider_registration.py +55 -16
- ksadk/studio/entry.py +53 -0
- ksadk/studio/event_store.py +53 -5
- ksadk/studio/execution_host.py +598 -0
- ksadk/studio/framework_run.py +50 -10
- ksadk/studio/hosted_kernel.py +1 -1
- ksadk/studio/interaction_submission.py +265 -0
- ksadk/studio/kernel_registry.py +429 -0
- ksadk/studio/kernel_sqlite_migration.py +181 -0
- ksadk/studio/lifecycle_status.py +43 -0
- ksadk/studio/model_buffered_wire.py +227 -0
- ksadk/studio/model_chat_tools.py +143 -0
- ksadk/studio/model_client.py +1939 -345
- ksadk/studio/model_content_stream.py +129 -0
- ksadk/studio/model_credentials.py +196 -0
- ksadk/studio/model_profile_service.py +89 -40
- ksadk/studio/model_request_projection.py +284 -0
- ksadk/studio/model_responses_content.py +296 -0
- ksadk/studio/model_usage.py +109 -0
- ksadk/studio/model_wire_failure.py +110 -0
- ksadk/studio/model_wire_identity.py +138 -0
- ksadk/studio/model_wire_snapshot.py +45 -0
- ksadk/studio/model_wire_tools.py +283 -0
- ksadk/studio/native_acceptance.py +68 -0
- ksadk/studio/native_approval.py +111 -0
- ksadk/studio/native_browser_port.py +861 -0
- ksadk/studio/native_child_mcp.py +87 -0
- ksadk/studio/native_child_publication.py +68 -0
- ksadk/studio/native_child_skills.py +72 -0
- ksadk/studio/native_child_workspace.py +176 -0
- ksadk/studio/native_command_policy.py +53 -0
- ksadk/studio/native_context_port.py +326 -0
- ksadk/studio/native_context_registration.py +54 -0
- ksadk/studio/native_document_input.py +88 -0
- ksadk/studio/native_documents.py +172 -0
- ksadk/studio/native_final_shape.py +852 -0
- ksadk/studio/native_gateway_stream.py +69 -0
- ksadk/studio/native_hook_port.py +145 -0
- ksadk/studio/native_hook_registration.py +70 -0
- ksadk/studio/native_inventory_disclosure.py +223 -0
- ksadk/studio/native_kernel_admission.py +84 -0
- ksadk/studio/native_language_diagnostics.py +320 -0
- ksadk/studio/native_mcp_disclosure.py +198 -0
- ksadk/studio/native_mcp_tool_port.py +219 -0
- ksadk/studio/native_memory_acceptance.py +362 -0
- ksadk/studio/native_memory_context.py +266 -0
- ksadk/studio/native_memory_flush.py +174 -0
- ksadk/studio/native_memory_sources.py +80 -0
- ksadk/studio/native_memory_tool_port.py +360 -0
- ksadk/studio/native_memory_turn.py +183 -0
- ksadk/studio/native_model_port.py +765 -0
- ksadk/studio/native_model_recovery.py +126 -0
- ksadk/studio/native_options.py +75 -0
- ksadk/studio/native_output_fields.py +58 -0
- ksadk/studio/native_planning.py +156 -0
- ksadk/studio/native_product.py +988 -0
- ksadk/studio/native_read_budget.py +426 -0
- ksadk/studio/native_recovery.py +171 -0
- ksadk/studio/native_scope.py +35 -0
- ksadk/studio/native_session_context_port.py +141 -0
- ksadk/studio/native_session_engine.py +14 -0
- ksadk/studio/native_skill_source.py +74 -0
- ksadk/studio/native_subagent_port.py +700 -0
- ksadk/studio/native_terminal_recovery.py +95 -0
- ksadk/studio/native_test_action.py +610 -0
- ksadk/studio/native_tool_port.py +477 -0
- ksadk/studio/native_user_input.py +60 -0
- ksadk/studio/native_verification_presentation.py +65 -0
- ksadk/studio/native_workspace_diagnostics.py +183 -0
- ksadk/studio/native_workspace_port.py +43 -0
- ksadk/studio/operations.py +34 -9
- ksadk/studio/plugin_composition.py +39 -12
- ksadk/studio/plugin_kernel_adapter.py +89 -0
- ksadk/studio/plugin_lifecycle.py +80 -0
- ksadk/studio/plugin_native_activation.py +163 -0
- ksadk/studio/plugin_runtime.py +832 -99
- ksadk/studio/plugin_runtime_admission.py +57 -0
- ksadk/studio/presentation.py +92 -0
- ksadk/studio/provider_recovery.py +59 -0
- ksadk/studio/public_output_store.py +69 -0
- ksadk/studio/recovery_page.py +70 -0
- ksadk/studio/resource_authority.py +69 -34
- ksadk/studio/resource_catalog.py +14 -12
- ksadk/studio/run_activity.py +848 -0
- ksadk/studio/run_controls.py +185 -0
- ksadk/studio/run_documents.py +127 -0
- ksadk/studio/run_service.py +1220 -335
- ksadk/studio/run_submission_receipts.py +68 -0
- ksadk/studio/runtime_catalog.py +73 -11
- ksadk/studio/runtime_prewarm.py +22 -0
- ksadk/studio/runtime_source.py +1 -1
- ksadk/studio/scheduler_assistant.py +4 -1
- ksadk/studio/scheduler_runtime.py +32 -289
- ksadk/studio/service.py +615 -246
- ksadk/studio/service_settings.py +229 -0
- ksadk/studio/shared_web.py +518 -181
- ksadk/studio/shared_web_response_events.py +180 -0
- ksadk/studio/shared_web_run_state.py +133 -0
- ksadk/studio/skill_discovery.py +305 -3
- ksadk/studio/static/assets/ChannelsPage-xX5HlMId.js +8 -0
- ksadk/studio/static/assets/CodeBlock-DMIf4h33-CkufcPYo.js +44 -0
- ksadk/studio/static/assets/ExecutionTree-v3HexvPo-Bk8wzGSy.js +1 -0
- ksadk/studio/static/assets/MathMessageMarkdown-BesSeCYl-DYG1Wzbx.js +8 -0
- ksadk/studio/static/assets/MermaidBlock-H_0qzQDf-BdXtxug_.js +11 -0
- ksadk/studio/static/assets/MessageMarkdown-CwMTvgFj-BeyLyQHu.js +59 -0
- ksadk/studio/static/assets/PrismRenderer-5pK6tUdb.js +3 -0
- ksadk/studio/static/assets/TeamsPage-CCaaGaVp.js +3 -0
- ksadk/studio/static/assets/abnfDiagram-VCTEODGH-Bnb1O3tA.js +1 -0
- ksadk/studio/static/assets/architecture-7GRP2DOG-BMOPAkUC.js +1 -0
- ksadk/studio/static/assets/architectureDiagram-5GKGNRK7-5Zn1gqJv.js +36 -0
- ksadk/studio/static/assets/{blockDiagram-I7D4REHJ-CFmPl1de.js → blockDiagram-I7D4REHJ-CLB4L11u.js} +6 -6
- ksadk/studio/static/assets/{c4Diagram-7LVT6UL2-BRrGu8qt.js → c4Diagram-7LVT6UL2-BOma_Z_F.js} +7 -7
- ksadk/studio/static/assets/channel-r8itPEik.js +1 -0
- ksadk/studio/static/assets/chunk-2E4U76K2-BFf_huql.js +2 -0
- ksadk/studio/static/assets/chunk-2ZTRR5NV-C2qZJila.js +2 -0
- ksadk/studio/static/assets/chunk-3Z5EZCMW-DMniOlPd.js +1 -0
- ksadk/studio/static/assets/{chunk-4HAMMTFA-D220vUtq.js → chunk-4HAMMTFA-DVtDP4rm.js} +7 -7
- ksadk/studio/static/assets/chunk-5V3GS4D5-i-ZDOEnP.js +1 -0
- ksadk/studio/static/assets/chunk-6AZGARVD-BB6FqMl-.js +1 -0
- ksadk/studio/static/assets/chunk-6EIED4P4-DdUJkP0f.js +1 -0
- ksadk/studio/static/assets/chunk-6K3QC6MW-CqVsVn-y.js +1 -0
- ksadk/studio/static/assets/chunk-6TQVIW2G-DKlLbNIP.js +1 -0
- ksadk/studio/static/assets/chunk-747NJXEK-BFkZPlab.js +2 -0
- ksadk/studio/static/assets/chunk-75Z2AOVW-BZNhM4vB.js +2 -0
- ksadk/studio/static/assets/chunk-CLGD4ZFX-D-GTtyKs.js +1 -0
- ksadk/studio/static/assets/{chunk-DU6HZSFF-C4DtVpCu.js → chunk-DU6HZSFF-CpEZCUgg.js} +1 -1
- ksadk/studio/static/assets/chunk-F27PBJKO-D7pvKf0C.js +1 -0
- ksadk/studio/static/assets/{chunk-GMAD6QVW-DiQA-u2N.js → chunk-GMAD6QVW-DE_8H717.js} +6 -6
- ksadk/studio/static/assets/chunk-GVQU2GXP-BfEB0lNv.js +1 -0
- ksadk/studio/static/assets/chunk-I5DQTOEV-Hm2LfiYI.js +1 -0
- ksadk/studio/static/assets/chunk-ICYGCRZG-749o-pGJ.js +1 -0
- ksadk/studio/static/assets/chunk-IH6LHLGP-CC6I9F3i.js +1 -0
- ksadk/studio/static/assets/{chunk-IMKFNOWR-BSlbjT_b.js → chunk-IMKFNOWR-D38axYOR.js} +5 -5
- ksadk/studio/static/assets/chunk-KI3K4JFJ-Rma-0k_M.js +1 -0
- ksadk/studio/static/assets/chunk-L3NEJ4N5-Dq5xn7aL.js +1 -0
- ksadk/studio/static/assets/chunk-LNGE3PJU-DWpGwHrW.js +34 -0
- ksadk/studio/static/assets/{chunk-OSK3NFVY-M2m6G3j4.js → chunk-OSK3NFVY-DiCQjx9b.js} +5 -5
- ksadk/studio/static/assets/chunk-OUJLGHUK-D0j2JRSg.js +2 -0
- ksadk/studio/static/assets/{chunk-P2QGCYS3-DezaQxiK.js → chunk-P2QGCYS3-VdhKmfYq.js} +1 -1
- ksadk/studio/static/assets/chunk-POPQ4Y6H-C1_rzO5M.js +1 -0
- ksadk/studio/static/assets/chunk-PWAF6VOD-B45HvflI.js +1 -0
- ksadk/studio/static/assets/{chunk-SHT3W25Y-DAyIVp5H.js → chunk-SHT3W25Y-DsCFnRVE.js} +9 -9
- ksadk/studio/static/assets/{chunk-SVP7TREG-DulomOrS.js → chunk-SVP7TREG-DljApgRX.js} +1 -1
- ksadk/studio/static/assets/{chunk-TICWLB2K-bHJYoflj.js → chunk-TICWLB2K-DWqRtyqa.js} +6 -6
- ksadk/studio/static/assets/chunk-TLUHSLCS-DUeGgnD5.js +2 -0
- ksadk/studio/static/assets/chunk-UY3FDG6J-CnmaAkG5.js +1 -0
- ksadk/studio/static/assets/chunk-XHIXRSVI-BFQ6B8BE.js +1 -0
- ksadk/studio/static/assets/chunk-XXDRQBXY-DqAYeljN.js +1 -0
- ksadk/studio/static/assets/classDiagram-ZZMXUADV-DjeMNJmu.js +1 -0
- ksadk/studio/static/assets/classDiagram-v2-VYDZK3BY-DjeMNJmu.js +1 -0
- ksadk/studio/static/assets/clsx-DB0hHKMi.js +1 -0
- ksadk/studio/static/assets/cose-bilkent-JH36ORCC-om8AY0Mc.js +1 -0
- ksadk/studio/static/assets/createLucideIcon-DR0LGTP5.js +1 -0
- ksadk/studio/static/assets/cynefin-OW5HDTMX-7tP7QfM3.js +1 -0
- ksadk/studio/static/assets/cynefinDiagram-5FMLGOSQ-6BrmK52S.js +62 -0
- ksadk/studio/static/assets/dagre-GXQ25YYZ-buZBRlwz.js +4 -0
- ksadk/studio/static/assets/{diagram-S7CK7UJ4-0BZ98mya.js → diagram-S7CK7UJ4-C6aH78c4.js} +3 -3
- ksadk/studio/static/assets/diagram-UQ7AKVKN-DY_tQ6C_.js +41 -0
- ksadk/studio/static/assets/diagram-VSXAHHWV-DX9p5cQT.js +3 -0
- ksadk/studio/static/assets/diagram-VX7I27RA-DB2WSH8X.js +24 -0
- ksadk/studio/static/assets/diagram-Z3DM3KII-Ce1BJwHE.js +24 -0
- ksadk/studio/static/assets/ebnfDiagram-PWID7BFC-Daukxwqf.js +1 -0
- ksadk/studio/static/assets/{erDiagram-RLTQ6QDP-DLGry2Ap.js → erDiagram-RLTQ6QDP-CGthTMmJ.js} +6 -6
- ksadk/studio/static/assets/eventmodeling-NTZA5JFV-BR2FzK4R.js +1 -0
- ksadk/studio/static/assets/flowDiagram-HODETNUW-CZ1tEenp.js +1 -0
- ksadk/studio/static/assets/ganttDiagram-EL5Y4UJY-BfoAZW9X.js +292 -0
- ksadk/studio/static/assets/gitGraph-4MIJSDKK-DYlXv_yJ.js +1 -0
- ksadk/studio/static/assets/gitGraphDiagram-WWUBYQGX-S985OIJF.js +106 -0
- ksadk/studio/static/assets/iconfont-4HN3C9Pg.woff2 +0 -0
- ksadk/studio/static/assets/index-C7DNDik2.css +2 -0
- ksadk/studio/static/assets/index-Cy68gTlH.js +265 -0
- ksadk/studio/static/assets/index.dom-BQenBtFm.js +1 -0
- ksadk/studio/static/assets/info-A6RAGUB7-BlU-8_xu.js +1 -0
- ksadk/studio/static/assets/infoDiagram-27XIBGKW-CO3W8tFn.js +2 -0
- ksadk/studio/static/assets/{ishikawaDiagram-5VMMS53U-CljI2wyf.js → ishikawaDiagram-5VMMS53U-BUGEl8cO.js} +2 -2
- ksadk/studio/static/assets/{journeyDiagram-3NMN7TZE-D50Fo2sL.js → journeyDiagram-3NMN7TZE-Ch2ZYxvr.js} +6 -6
- ksadk/studio/static/assets/{kanban-definition-UXKFOSKX-DimfOTam.js → kanban-definition-UXKFOSKX-Da-Fp4vC.js} +3 -3
- ksadk/studio/static/assets/kingsoft-cloud-BOcLiQ3e.ico +0 -0
- ksadk/studio/static/assets/{line-CX5BOOUP.js → line-BDaeq300.js} +1 -1
- ksadk/studio/static/assets/linear-DmucvK78.js +1 -0
- ksadk/studio/static/assets/mermaid-parser.core-Cj5mYdBi.js +4 -0
- ksadk/studio/static/assets/{mindmap-definition-YA3MSWOX-Bizzqy5R.js → mindmap-definition-YA3MSWOX-CvlsVQ38.js} +1 -1
- ksadk/studio/static/assets/packet-AYTQ26CC-BSJEH9Lb.js +1 -0
- ksadk/studio/static/assets/pegDiagram-XKGWAZYB-BuHuNgT5.js +1 -0
- ksadk/studio/static/assets/pie-WAS4IAKB-qxv01Y8W.js +1 -0
- ksadk/studio/static/assets/{pieDiagram-E7YTZNPT-B9mrFSDF.js → pieDiagram-E7YTZNPT-DlrWiSOv.js} +1 -1
- ksadk/studio/static/assets/preload-helper-CO68xz6u.js +1 -0
- ksadk/studio/static/assets/presentation-yYw7xK60-BmcZriri.js +1 -0
- ksadk/studio/static/assets/{quadrantDiagram-AXDQQJYC-NzUfCbMR.js → quadrantDiagram-AXDQQJYC-BA3hpUZM.js} +6 -6
- ksadk/studio/static/assets/radar-RG4KPBEZ-B6h6pWOs.js +1 -0
- ksadk/studio/static/assets/railroad-74A4TZTK-CGtTWdGc.js +1 -0
- ksadk/studio/static/assets/railroad-abnf-HS5TGJTU-Cq7y22IA.js +1 -0
- ksadk/studio/static/assets/railroad-ebnf-LZEXJU2U-V0aQVv9m.js +1 -0
- ksadk/studio/static/assets/railroad-peg-WCYAUIDC-BQiIsX_p.js +1 -0
- ksadk/studio/static/assets/railroadDiagram-O6MQD6OU-BxpAazym.js +1 -0
- ksadk/studio/static/assets/{requirementDiagram-BXWQKSXE-BF3d103U.js → requirementDiagram-BXWQKSXE-E-Ge0FcV.js} +1 -1
- ksadk/studio/static/assets/sankeyDiagram-P5KCCOFB-CA6YKhQ3.js +40 -0
- ksadk/studio/static/assets/{sequenceDiagram-WJ2MYXX4-Dh4wZtUm.js → sequenceDiagram-WJ2MYXX4-CcElXGg4.js} +9 -9
- ksadk/studio/static/assets/src-B6xuSHsQ.js +1 -0
- ksadk/studio/static/assets/stateDiagram-D77RDMKH-DdXZOFqY.js +1 -0
- ksadk/studio/static/assets/stateDiagram-v2-MP3YSRHH-CJbOM-QK.js +1 -0
- ksadk/studio/static/assets/swimlanes-42K2YHIH-NtgEEHLY.js +1 -0
- ksadk/studio/static/assets/swimlanesDiagram-VR7AAH4N-D-AVlW8B.js +8 -0
- ksadk/studio/static/assets/{timeline-definition-24CTP7MA-DxmSSjRB.js → timeline-definition-24CTP7MA-CIuYhNy3.js} +7 -7
- ksadk/studio/static/assets/treeView-Q6P3EWNA-BTTYcvrF.js +1 -0
- ksadk/studio/static/assets/treemap-WGGIJYW6-Cg7tyfH8.js +1 -0
- ksadk/studio/static/assets/vennDiagram-4TSXK5OY-7HbApbrv.js +34 -0
- ksadk/studio/static/assets/wardley-WFR3VGLG-C6N1rULY.js +1 -0
- ksadk/studio/static/assets/{wardleyDiagram-VM6X3IG4-Ba1AOWoe.js → wardleyDiagram-VM6X3IG4-CMJiaedy.js} +1 -1
- ksadk/studio/static/assets/{xychartDiagram-S5SC5T6Z-Cr5pt5Y-.js → xychartDiagram-S5SC5T6Z-BdgtMvW2.js} +6 -6
- ksadk/studio/static/index.html +8 -2
- ksadk/studio/teams_catalog.py +166 -0
- ksadk/studio/teams_installation.py +217 -0
- ksadk/studio/teams_node.py +549 -0
- ksadk/studio/teams_node_factory.py +558 -0
- ksadk/studio/teams_node_runner.py +72 -0
- ksadk/studio/teams_node_v1.py +935 -0
- ksadk/studio/teams_remote.py +496 -0
- ksadk/studio/templates.py +41 -19
- ksadk/studio/validator.py +13 -0
- ksadk/studio/workspace.py +19 -4
- ksadk/studio/workspace_plugins.py +138 -0
- ksadk/studio/workspace_registry.py +286 -0
- ksadk/tools/a2a_policy.py +5 -0
- ksadk/tools/artifact_observation.py +53 -0
- ksadk/tools/captured_source_artifact.py +68 -0
- ksadk/tools/schema.py +34 -0
- ksadk/toolsets/__init__.py +93 -88
- ksadk/toolsets/a2a.py +89 -89
- ksadk/toolsets/sandbox.py +377 -6
- ksadk/toolsets/skill_management.py +315 -0
- ksadk/toolsets/skills.py +446 -378
- ksadk/toolsets/tool_selection.py +94 -0
- ksadk/toolsets/workspace.py +344 -326
- ksadk/toolsets/workspace_editing.py +291 -0
- ksadk/toolsets/workspace_identity.py +15 -0
- ksadk/toolsets/workspace_model_observation.py +104 -0
- ksadk/toolsets/workspace_mutation.py +75 -0
- ksadk/toolsets/workspace_read_budget.py +216 -0
- ksadk/toolsets/workspace_search.py +192 -0
- ksadk/toolsets/workspace_search_process.py +129 -0
- ksadk/toolsets/workspace_state.py +6 -0
- ksadk/version.py +1 -1
- ksadk_runtime_common/http_headers.py +18 -0
- ksadk_runtime_common/memory_backend/providers/lancedb.py +56 -2
- ksadk_runtime_common/schemas/memory_backend_manifest.schema.json +148 -148
- ksadk_runtime_common/workspace_files/router.py +28 -14
- agentengine_sdk_python-0.8.4.dist-info/RECORD +0 -1070
- ksadk/server/static/assets/MathMessageMarkdown-DbdW5A4_.js +0 -8
- ksadk/server/static/assets/architecture-7GRP2DOG-BW5UtSnx.js +0 -1
- ksadk/server/static/assets/channel-CF5Up3hQ.js +0 -1
- ksadk/server/static/assets/chunk-2Q5K7J3B-KkXX47WI.js +0 -1
- ksadk/server/static/assets/classDiagram-DTDB5LWJ-Cz81acRQ.js +0 -1
- ksadk/server/static/assets/classDiagram-v2-JRS7N3AN-Cz81acRQ.js +0 -1
- ksadk/server/static/assets/cynefin-OW5HDTMX-DVWs8a2t.js +0 -1
- ksadk/server/static/assets/dist-B2_CVgKv.js +0 -1
- ksadk/server/static/assets/eventmodeling-NTZA5JFV-CQqR2hg3.js +0 -1
- ksadk/server/static/assets/flowDiagram-A5DVABFB-DDRRbGPm.js +0 -1
- ksadk/server/static/assets/gitGraph-4MIJSDKK-fgWEf7UD.js +0 -1
- ksadk/server/static/assets/index-DcTML1Up.css +0 -2
- ksadk/server/static/assets/index-Dh2XHqPK.js +0 -244
- ksadk/server/static/assets/info-A6RAGUB7-BUecLTdt.js +0 -1
- ksadk/server/static/assets/packet-AYTQ26CC-BeW-btFv.js +0 -1
- ksadk/server/static/assets/pie-WAS4IAKB-X0xh4SaT.js +0 -1
- ksadk/server/static/assets/radar-RG4KPBEZ-DzRMSqjd.js +0 -1
- ksadk/server/static/assets/railroad-74A4TZTK-Cc44icX-.js +0 -1
- ksadk/server/static/assets/railroad-abnf-HS5TGJTU-D6S4BdV-.js +0 -1
- ksadk/server/static/assets/railroad-ebnf-LZEXJU2U-vD3aVE2o.js +0 -1
- ksadk/server/static/assets/railroad-peg-WCYAUIDC-CYbW_b4T.js +0 -1
- ksadk/server/static/assets/stateDiagram-v2-4QOOHH4V-BIcjKFhF.js +0 -1
- ksadk/server/static/assets/swimlanesDiagram-VK2B7HYN-tq3K3Ybl.js +0 -8
- ksadk/server/static/assets/treeView-Q6P3EWNA-DBXt3GdU.js +0 -1
- ksadk/server/static/assets/treemap-WGGIJYW6-DrCy4E_l.js +0 -1
- ksadk/server/static/assets/wardley-WFR3VGLG-CefhOliM.js +0 -1
- ksadk/studio/static/assets/CodeBlock-DCUUY0PL-BLjA7kbS.js +0 -41
- ksadk/studio/static/assets/MathMessageMarkdown-CBcDrAw--CUYIXwgI.js +0 -8
- ksadk/studio/static/assets/MermaidBlock-DBFTztfQ-BTYKWEbD.js +0 -44
- ksadk/studio/static/assets/PrismRenderer-wiXRHURw.js +0 -3
- ksadk/studio/static/assets/abnfDiagram-VCTEODGH-CLETGjKq.js +0 -1
- ksadk/studio/static/assets/architecture-7GRP2DOG-hXMiO_AU.js +0 -1
- ksadk/studio/static/assets/architectureDiagram-5GKGNRK7-B5EQysk6.js +0 -36
- ksadk/studio/static/assets/channel-BqO-gtEw.js +0 -1
- ksadk/studio/static/assets/chunk-75Z2AOVW-C8lLv_qc.js +0 -2
- ksadk/studio/static/assets/chunk-F27PBJKO-Cpw14SNG.js +0 -1
- ksadk/studio/static/assets/chunk-GVQU2GXP-DUdNBUCi.js +0 -1
- ksadk/studio/static/assets/chunk-L3NEJ4N5-Db8Ab22W.js +0 -1
- ksadk/studio/static/assets/chunk-POPQ4Y6H-DOpufJ3t.js +0 -1
- ksadk/studio/static/assets/chunk-PWAF6VOD-CfLb1gSc.js +0 -1
- ksadk/studio/static/assets/chunk-XXDRQBXY-7PcdR2B1.js +0 -1
- ksadk/studio/static/assets/classDiagram-ZZMXUADV-D_bW8JjA.js +0 -1
- ksadk/studio/static/assets/classDiagram-v2-VYDZK3BY-D_bW8JjA.js +0 -1
- ksadk/studio/static/assets/cose-bilkent-JH36ORCC-snL88wkX.js +0 -1
- ksadk/studio/static/assets/cynefin-OW5HDTMX-Bd1YhRo1.js +0 -1
- ksadk/studio/static/assets/cynefinDiagram-5FMLGOSQ-BBrIG_mI.js +0 -62
- ksadk/studio/static/assets/dagre-GXQ25YYZ-DtF1nGdY.js +0 -4
- ksadk/studio/static/assets/diagram-UQ7AKVKN-6mLKEo6L.js +0 -41
- ksadk/studio/static/assets/diagram-VSXAHHWV-CJBIaiZ2.js +0 -3
- ksadk/studio/static/assets/diagram-VX7I27RA-BohLEE1C.js +0 -24
- ksadk/studio/static/assets/diagram-Z3DM3KII-D8KIGonG.js +0 -24
- ksadk/studio/static/assets/ebnfDiagram-PWID7BFC-C99HU61t.js +0 -1
- ksadk/studio/static/assets/eventmodeling-NTZA5JFV-B1_5Ac8j.js +0 -1
- ksadk/studio/static/assets/flowDiagram-HODETNUW-DD5hmicn.js +0 -1
- ksadk/studio/static/assets/ganttDiagram-EL5Y4UJY-BPJrVLSG.js +0 -292
- ksadk/studio/static/assets/gitGraph-4MIJSDKK-Cy_ZQBGi.js +0 -1
- ksadk/studio/static/assets/gitGraphDiagram-WWUBYQGX-CZCUz4MT.js +0 -106
- ksadk/studio/static/assets/index-CeHL9ARV.css +0 -2
- ksadk/studio/static/assets/index-D9QEcVpL.js +0 -320
- ksadk/studio/static/assets/info-A6RAGUB7-DBzDQB_I.js +0 -1
- ksadk/studio/static/assets/infoDiagram-27XIBGKW-B-sq07Hi.js +0 -2
- ksadk/studio/static/assets/linear-BCxN9JLv.js +0 -1
- ksadk/studio/static/assets/mermaid-parser.core-BdCz8fkS.js +0 -7
- ksadk/studio/static/assets/packet-AYTQ26CC-gPln-_kG.js +0 -1
- ksadk/studio/static/assets/pegDiagram-XKGWAZYB-Dy3fB-gW.js +0 -1
- ksadk/studio/static/assets/pie-WAS4IAKB-qZdHMr7B.js +0 -1
- ksadk/studio/static/assets/radar-RG4KPBEZ-BZlnaea9.js +0 -1
- ksadk/studio/static/assets/railroad-74A4TZTK-Ce3__H9c.js +0 -1
- ksadk/studio/static/assets/railroad-abnf-HS5TGJTU-DqQn7EzD.js +0 -1
- ksadk/studio/static/assets/railroad-ebnf-LZEXJU2U-B55kCA8F.js +0 -1
- ksadk/studio/static/assets/railroad-peg-WCYAUIDC-DfQnvkHd.js +0 -1
- ksadk/studio/static/assets/railroadDiagram-O6MQD6OU-DnhMjZCk.js +0 -1
- ksadk/studio/static/assets/sankeyDiagram-P5KCCOFB-Bo-JTwt0.js +0 -40
- ksadk/studio/static/assets/src-SFn9St9H.js +0 -1
- ksadk/studio/static/assets/stateDiagram-D77RDMKH-DH774G6d.js +0 -1
- ksadk/studio/static/assets/stateDiagram-v2-MP3YSRHH-DZvVWPBo.js +0 -1
- ksadk/studio/static/assets/swimlanes-42K2YHIH-Bl13OvTX.js +0 -1
- ksadk/studio/static/assets/swimlanesDiagram-VR7AAH4N-BnFOTALJ.js +0 -8
- ksadk/studio/static/assets/treeView-Q6P3EWNA-D5Nn9Swz.js +0 -1
- ksadk/studio/static/assets/treemap-WGGIJYW6-CgHgnQhE.js +0 -1
- ksadk/studio/static/assets/vennDiagram-4TSXK5OY-B9QEXcJq.js +0 -34
- ksadk/studio/static/assets/wardley-WFR3VGLG-DXrK2RRY.js +0 -1
- {agentengine_sdk_python-0.8.4.dist-info → agentengine_sdk_python-0.8.6.dist-info}/WHEEL +0 -0
- {agentengine_sdk_python-0.8.4.dist-info → agentengine_sdk_python-0.8.6.dist-info}/entry_points.txt +0 -0
- {agentengine_sdk_python-0.8.4.dist-info → agentengine_sdk_python-0.8.6.dist-info}/licenses/LICENSE +0 -0
- {agentengine_sdk_python-0.8.4.dist-info → agentengine_sdk_python-0.8.6.dist-info}/top_level.txt +0 -0
- /ksadk/studio/static/assets/{MathMessageMarkdown-CBcDrAw--BEOyPC3w.css → MathMessageMarkdown-BesSeCYl-BEOyPC3w.css} +0 -0
ksadk/studio/model_client.py
CHANGED
|
@@ -3,18 +3,54 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import hashlib
|
|
7
|
+
import html
|
|
6
8
|
import ipaddress
|
|
7
|
-
import
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
8
12
|
import socket
|
|
9
|
-
|
|
13
|
+
import time
|
|
14
|
+
from collections.abc import AsyncIterator
|
|
15
|
+
from contextlib import asynccontextmanager
|
|
16
|
+
from dataclasses import dataclass, field, replace
|
|
10
17
|
from threading import RLock
|
|
11
|
-
from typing import Any, Awaitable, Callable
|
|
18
|
+
from typing import Any, Awaitable, Callable, Literal
|
|
12
19
|
from urllib.parse import urlparse
|
|
13
20
|
|
|
14
21
|
import httpx
|
|
15
22
|
|
|
16
|
-
from ksadk.
|
|
23
|
+
from ksadk._async_iterators import owning_async_context, owning_async_iterator
|
|
24
|
+
from ksadk._model_input import ProviderInputObservation, input_observation
|
|
25
|
+
from ksadk._model_json import ModelJSONPolicyError, load_model_json
|
|
26
|
+
from ksadk._model_sse import ModelSSEIdleTimeout, SSEFrame, SSEFramingError, model_sse_frames
|
|
27
|
+
from ksadk.model_proxy import compat as provider_compat
|
|
28
|
+
from ksadk.studio.contracts import ANTHROPIC_VERSION, NetworkPolicy, ResolvedModel, Usage
|
|
17
29
|
from ksadk.studio.errors import StudioError
|
|
30
|
+
from ksadk.studio.model_buffered_wire import (
|
|
31
|
+
buffered_projection,
|
|
32
|
+
validate_buffered_identity,
|
|
33
|
+
wire_refusal,
|
|
34
|
+
)
|
|
35
|
+
from ksadk.studio.model_chat_tools import ChatToolCalls
|
|
36
|
+
from ksadk.studio.model_content_stream import AnthropicContent, ChatContent
|
|
37
|
+
from ksadk.studio.model_request_projection import anthropic_messages, responses_messages
|
|
38
|
+
from ksadk.studio.model_responses_content import ResponsesContent
|
|
39
|
+
from ksadk.studio.model_usage import AnthropicUsageAccumulator, provider_usage
|
|
40
|
+
from ksadk.studio.model_wire_failure import ModelResponseError, provider_error
|
|
41
|
+
from ksadk.studio.model_wire_identity import (
|
|
42
|
+
ModelWireIdentity,
|
|
43
|
+
chat_choices,
|
|
44
|
+
decode_model_frame,
|
|
45
|
+
invalid_model_frame,
|
|
46
|
+
)
|
|
47
|
+
from ksadk.studio.model_wire_snapshot import reconcile_responses_snapshot
|
|
48
|
+
from ksadk.studio.model_wire_tools import AnthropicToolBlocks, ResponsesToolCalls
|
|
49
|
+
|
|
50
|
+
from .model_credentials import CredentialResolver as CredentialResolver
|
|
51
|
+
|
|
52
|
+
#: Anthropic Messages wire 把 max_tokens 设为必填;profile 未配置时的安全默认。
|
|
53
|
+
_ANTHROPIC_DEFAULT_MAX_TOKENS = 8192
|
|
18
54
|
|
|
19
55
|
_DENIED_METADATA_HOSTS = {
|
|
20
56
|
"169.254.169.254",
|
|
@@ -28,6 +64,132 @@ _LENGTH_FINISH_REASONS = {"length", "incomplete", "max_output_tokens"}
|
|
|
28
64
|
#: length 截断重试时把 max_tokens 提到的目标值;未配置 max_tokens 的 profile
|
|
29
65
|
#: 首次截断重试也用该值兜底(authoring 输出完整 JSON 需要宽松上限)。
|
|
30
66
|
_LENGTH_RETRY_TARGET_TOKENS = 16384
|
|
67
|
+
# These explicit terminal reasons cannot authorize any partially assembled tool.
|
|
68
|
+
# Preserve them even when the provider ended before emitting its arguments.
|
|
69
|
+
_INCOMPLETE_CALL_NON_SUCCESS_FINISH_REASONS = {
|
|
70
|
+
"length", "incomplete", "max_tokens", "max_output_tokens", "output_limit",
|
|
71
|
+
"context_overflow", "context_length_exceeded", "policy_abort", "content_filter",
|
|
72
|
+
"user_abort", "canceled", "cancelled",
|
|
73
|
+
}
|
|
74
|
+
_REPETITION_MIN_UNIT_CHARS = 24
|
|
75
|
+
_REPETITION_MAX_UNIT_CHARS = 512
|
|
76
|
+
_REPETITION_COUNT = 4
|
|
77
|
+
|
|
78
|
+
_LOGGER = logging.getLogger(__name__)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def _http_model_error(response: httpx.Response, *, wire_api: str) -> StudioError:
|
|
82
|
+
if response.is_redirect:
|
|
83
|
+
return StudioError("NETWORK_TARGET_DENIED", "模型 endpoint 不允许重定向", status_code=403,
|
|
84
|
+
details={"statusCode": response.status_code})
|
|
85
|
+
payload = {}
|
|
86
|
+
usage = None
|
|
87
|
+
try:
|
|
88
|
+
raw = await response.aread()
|
|
89
|
+
if len(raw) <= 16 * 1024 * 1024:
|
|
90
|
+
payload = decode_model_frame(raw)
|
|
91
|
+
validate_buffered_identity(payload, wire_api=wire_api)
|
|
92
|
+
usage = provider_usage(payload.get("usage"), wire_api=wire_api)
|
|
93
|
+
except (StudioError, httpx.HTTPError):
|
|
94
|
+
# An unreadable error body cannot erase the actual HTTP failure fact.
|
|
95
|
+
payload, usage = {}, None
|
|
96
|
+
failure = provider_error(
|
|
97
|
+
payload, wire_api=wire_api, status=response.status_code, usage=usage,
|
|
98
|
+
headers=response.headers, provider_request_id=_provider_request_id(response.headers),
|
|
99
|
+
)
|
|
100
|
+
assert failure is not None
|
|
101
|
+
return failure
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@asynccontextmanager
|
|
105
|
+
async def _model_frames(
|
|
106
|
+
response: httpx.Response, *, idle_timeout_seconds: float,
|
|
107
|
+
known_policy_abort: Callable[[], bool] | None = None,
|
|
108
|
+
) -> AsyncIterator[AsyncIterator[SSEFrame]]:
|
|
109
|
+
try:
|
|
110
|
+
async with model_sse_frames(
|
|
111
|
+
response.aiter_bytes(), max_bytes=16 * 1024 * 1024,
|
|
112
|
+
idle_timeout_seconds=idle_timeout_seconds,
|
|
113
|
+
) as frames:
|
|
114
|
+
yield frames
|
|
115
|
+
except ModelSSEIdleTimeout:
|
|
116
|
+
if known_policy_abort is None or not known_policy_abort():
|
|
117
|
+
raise httpx.ReadTimeout("model stream idle timeout") from None
|
|
118
|
+
except (SSEFramingError, UnicodeError):
|
|
119
|
+
raise invalid_model_frame() from None
|
|
120
|
+
except httpx.TransportError:
|
|
121
|
+
# A typed refusal already observed in this attempt cannot become a
|
|
122
|
+
# retryable request just because the remaining transport tail is lost.
|
|
123
|
+
if known_policy_abort is None or not known_policy_abort():
|
|
124
|
+
raise
|
|
125
|
+
|
|
126
|
+
_DSML_INVOKE_RE = re.compile(
|
|
127
|
+
r"<\s*(?:\|\s*){1,2}DSML\s*(?:\|\s*){1,2}invoke\b(?P<attrs>[^>]*)>"
|
|
128
|
+
r"(?P<body>.*?)"
|
|
129
|
+
r"<\s*(?:\|\s*){1,2}DSML\s*(?:\|\s*){1,2}/invoke\s*>",
|
|
130
|
+
re.IGNORECASE | re.DOTALL,
|
|
131
|
+
)
|
|
132
|
+
_DSML_PARAMETER_RE = re.compile(
|
|
133
|
+
r"<\s*(?:\|\s*){1,2}DSML\s*(?:\|\s*){1,2}parameter\b(?P<attrs>[^>]*)>"
|
|
134
|
+
r"(?P<value>.*?)"
|
|
135
|
+
r"<\s*(?:\|\s*){1,2}DSML\s*(?:\|\s*){1,2}/parameter\s*>",
|
|
136
|
+
re.IGNORECASE | re.DOTALL,
|
|
137
|
+
)
|
|
138
|
+
_DSML_CALLS_WRAPPER_RE = re.compile(
|
|
139
|
+
r"<\s*(?:\|\s*){1,2}DSML\s*(?:\|\s*){1,2}/?calls\s*>",
|
|
140
|
+
re.IGNORECASE,
|
|
141
|
+
)
|
|
142
|
+
_DSML_ATTRIBUTE_RE = re.compile(
|
|
143
|
+
r"(?P<name>[A-Za-z_][\w.-]*)\s*=\s*(?P<quote>['\"])(?P<value>.*?)(?P=quote)",
|
|
144
|
+
re.DOTALL,
|
|
145
|
+
)
|
|
146
|
+
_DSML_SIMPLE_ARGUMENT_RE = re.compile(
|
|
147
|
+
r"<(?P<name>[A-Za-z_][\w.-]*)>(?P<value>.*?)</(?P=name)>",
|
|
148
|
+
re.DOTALL,
|
|
149
|
+
)
|
|
150
|
+
_DSML_STREAM_LOOKBEHIND = 64
|
|
151
|
+
_GLM_TOOL_CALL_RE = re.compile(
|
|
152
|
+
r"<tool_call>\s*(?P<name>[A-Za-z_][\w.-]*)\s*(?P<body>.*?)</tool_call>",
|
|
153
|
+
re.IGNORECASE | re.DOTALL,
|
|
154
|
+
)
|
|
155
|
+
_GLM_ARGUMENT_TAG_RE = re.compile(
|
|
156
|
+
r"<(?P<tag>arg_key|arg_value)>(?P<value>.*?)</(?:arg_key|arg_value)>",
|
|
157
|
+
re.IGNORECASE | re.DOTALL,
|
|
158
|
+
)
|
|
159
|
+
_GLM_ASSIGNMENT_RE = re.compile(
|
|
160
|
+
r"^(?P<name>[A-Za-z_][\w.-]*)\s*=\s*(?P<quote>['\"])(?P<value>.*)(?P=quote)$",
|
|
161
|
+
re.DOTALL,
|
|
162
|
+
)
|
|
163
|
+
_GLM_ARGUMENT_KEY_RESIDUE_RE = re.compile(
|
|
164
|
+
r"(?:</?\s*arg_(?:key|value)\s*>)+$",
|
|
165
|
+
re.IGNORECASE,
|
|
166
|
+
)
|
|
167
|
+
_GLM_MALFORMED_TOOL_START_RE = re.compile(
|
|
168
|
+
r"<\s*tool_call\s*>\s*(?P<name>[A-Za-z_][\w.-]*)",
|
|
169
|
+
re.IGNORECASE,
|
|
170
|
+
)
|
|
171
|
+
_GLM_MALFORMED_KEY_RE = re.compile(
|
|
172
|
+
r"(?:<\s*arg_value\s*>\s*)?(?P<name>[A-Za-z_][\w.-]*)\s*"
|
|
173
|
+
r"</\s*arg_key\s*>",
|
|
174
|
+
re.IGNORECASE,
|
|
175
|
+
)
|
|
176
|
+
_GLM_MALFORMED_ASSIGNMENT_RE = re.compile(
|
|
177
|
+
r"(?P<name>[A-Za-z_][\w.-]*)\s*=\s*(?P<quote>['\"])(?P<value>.*?)(?P=quote)"
|
|
178
|
+
r"\s*</\s*arg_value\s*>",
|
|
179
|
+
re.IGNORECASE | re.DOTALL,
|
|
180
|
+
)
|
|
181
|
+
_SAFE_TOOL_NAME_ALIASES = {
|
|
182
|
+
# GLM 5.2 has emitted this shorter DSML name for the explicitly
|
|
183
|
+
# advertised workspace writer. The alias is accepted only when the target
|
|
184
|
+
# tool is present in the current request's declared schema.
|
|
185
|
+
"writefile": "writeworkspacefile",
|
|
186
|
+
}
|
|
187
|
+
_SAFE_TOOL_ARGUMENT_ALIASES = {
|
|
188
|
+
"write_workspace_file": {
|
|
189
|
+
"filename": "path",
|
|
190
|
+
"filepath": "path",
|
|
191
|
+
},
|
|
192
|
+
}
|
|
31
193
|
|
|
32
194
|
|
|
33
195
|
@dataclass(frozen=True)
|
|
@@ -45,209 +207,580 @@ class ModelResponse:
|
|
|
45
207
|
usage: Usage
|
|
46
208
|
tool_calls: list[ToolCall]
|
|
47
209
|
raw_message: dict[str, Any]
|
|
210
|
+
#: 推理文本(chat wire 的 reasoning_content / Responses wire 的 reasoning 项)。
|
|
211
|
+
reasoning: str = ""
|
|
212
|
+
#: 工具回合只返回 reasoning_content、修复后仍无内容与工具调用(可见降级信号)。
|
|
213
|
+
empty_reasoning_only: bool = False
|
|
214
|
+
#: Upstream correlation id only; never contains authorization material.
|
|
215
|
+
provider_request_id: str = ""
|
|
216
|
+
provider_input: ProviderInputObservation | None = field(default=None, repr=False)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dataclass(frozen=True)
|
|
220
|
+
class ModelStreamChunk:
|
|
221
|
+
"""One OpenAI-compatible chat stream delta."""
|
|
222
|
+
|
|
223
|
+
text: str = ""
|
|
224
|
+
reasoning: str = ""
|
|
225
|
+
tool_calls: tuple[ToolCall, ...] = ()
|
|
226
|
+
usage: Usage | None = None
|
|
227
|
+
done: bool = False
|
|
228
|
+
# Preserve the provider terminal reason across the canonical gateway
|
|
229
|
+
# boundary. Dropping ``length`` made a truncated plan indistinguishable
|
|
230
|
+
# from a complete answer to Agent runtimes.
|
|
231
|
+
finish_reason: str = ""
|
|
232
|
+
provider_request_id: str = ""
|
|
233
|
+
# Complete provider-only replay at the terminal, never a public text delta.
|
|
234
|
+
provider_content: tuple[dict[str, Any], ...] | None = field(default=None, repr=False)
|
|
235
|
+
provider_input: ProviderInputObservation | None = field(default=None, repr=False)
|
|
236
|
+
public_summary: str = ""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _provider_request_id(headers: Any) -> str:
|
|
240
|
+
"""Extract a bounded upstream correlation id from common Gateway headers."""
|
|
241
|
+
|
|
242
|
+
for name in (
|
|
243
|
+
"x-request-id",
|
|
244
|
+
"request-id",
|
|
245
|
+
"x-amzn-requestid",
|
|
246
|
+
"x-trace-id",
|
|
247
|
+
"trace-id",
|
|
248
|
+
):
|
|
249
|
+
try:
|
|
250
|
+
value = str(headers.get(name) or "").strip()
|
|
251
|
+
except Exception: # noqa: BLE001 - third-party header mappings vary
|
|
252
|
+
value = ""
|
|
253
|
+
if value:
|
|
254
|
+
return value[:256]
|
|
255
|
+
return ""
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _dsml_attributes(value: str) -> dict[str, str]:
|
|
259
|
+
return {
|
|
260
|
+
match.group("name"): html.unescape(match.group("value"))
|
|
261
|
+
for match in _DSML_ATTRIBUTE_RE.finditer(value)
|
|
262
|
+
}
|
|
263
|
+
|
|
48
264
|
|
|
265
|
+
def _declared_tool_names(tools: list[dict[str, Any]] | None) -> dict[str, str]:
|
|
266
|
+
declared: dict[str, str] = {}
|
|
267
|
+
for tool in tools or []:
|
|
268
|
+
function = tool.get("function") or tool
|
|
269
|
+
name = str(function.get("name") or "").strip()
|
|
270
|
+
if name:
|
|
271
|
+
declared[re.sub(r"[^a-z0-9]", "", name.lower())] = name
|
|
272
|
+
return declared
|
|
49
273
|
|
|
50
|
-
class CredentialResolver:
|
|
51
|
-
"""Resolve immutable Secret references with a workspace-persisted overlay.
|
|
52
274
|
|
|
53
|
-
|
|
54
|
-
|
|
275
|
+
def _delegation_reasoning_effort(
|
|
276
|
+
model: ResolvedModel,
|
|
277
|
+
tools: list[dict[str, Any]] | None,
|
|
278
|
+
compatibility_profile: provider_compat.CompatProfile | None = None,
|
|
279
|
+
) -> str | None:
|
|
280
|
+
"""Return the provider-declared effort for a delegation decision.
|
|
281
|
+
|
|
282
|
+
Provider-specific values live in ``model_proxy.compat``. Studio only
|
|
283
|
+
identifies the semantic turn (the controller exposes ``delegate_task``),
|
|
284
|
+
avoiding transport branches keyed directly on model names.
|
|
55
285
|
"""
|
|
56
286
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
287
|
+
if "delegate_task" not in _declared_tool_names(tools).values():
|
|
288
|
+
return None
|
|
289
|
+
profile = compatibility_profile or provider_compat.compat_profile(
|
|
290
|
+
base_url=model.endpoint_url,
|
|
291
|
+
model=model.model,
|
|
292
|
+
)
|
|
293
|
+
return provider_compat.default_delegation_reasoning_effort(
|
|
294
|
+
profile,
|
|
295
|
+
model=model.model,
|
|
60
296
|
)
|
|
61
297
|
|
|
62
|
-
def __init__(self, workspace: Any = None) -> None:
|
|
63
|
-
self._session_values: dict[str, bytearray] = {}
|
|
64
|
-
self._lock = RLock()
|
|
65
|
-
self._workspace = workspace
|
|
66
|
-
self._persisted: dict[str, str] | None = None
|
|
67
298
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
299
|
+
def _effective_reasoning_effort(
|
|
300
|
+
model: ResolvedModel,
|
|
301
|
+
tools: list[dict[str, Any]] | None,
|
|
302
|
+
explicit: str | None,
|
|
303
|
+
compatibility_profile: provider_compat.CompatProfile | None = None,
|
|
304
|
+
) -> str | None:
|
|
305
|
+
"""Resolve effort with the same precedence as mature agent runtimes.
|
|
306
|
+
|
|
307
|
+
Explicit run/session selection wins. Only when it is absent may the
|
|
308
|
+
endpoint compatibility profile supply a narrow controller-turn default.
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
profile = compatibility_profile or provider_compat.compat_profile(
|
|
312
|
+
base_url=model.endpoint_url, model=model.model,
|
|
313
|
+
)
|
|
314
|
+
if not profile.supports_reasoning_effort:
|
|
315
|
+
return None
|
|
316
|
+
requested = explicit or _delegation_reasoning_effort(model, tools, profile)
|
|
317
|
+
if not requested:
|
|
318
|
+
return None
|
|
319
|
+
from ksadk.model_proxy.transform import clamp_reasoning_effort
|
|
320
|
+
|
|
321
|
+
return clamp_reasoning_effort(model.model, requested, policy=profile.reasoning_effort_policy)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _canonical_declared_tool_name(
|
|
325
|
+
requested_name: str,
|
|
326
|
+
declared: dict[str, str],
|
|
327
|
+
) -> str | None:
|
|
328
|
+
key = re.sub(r"[^a-z0-9]", "", requested_name.lower())
|
|
329
|
+
canonical = declared.get(key)
|
|
330
|
+
if canonical:
|
|
331
|
+
return canonical
|
|
332
|
+
alias_target = _SAFE_TOOL_NAME_ALIASES.get(key)
|
|
333
|
+
canonical = declared.get(alias_target or "")
|
|
334
|
+
if canonical:
|
|
335
|
+
return canonical
|
|
336
|
+
# GLM 5.2 occasionally appends a leaked textual call id to a declared
|
|
337
|
+
# name (for example ``write_file_ide49a``). Only strip this narrowly
|
|
338
|
+
# shaped suffix and only accept the prefix when it resolves to an
|
|
339
|
+
# explicitly advertised tool/alias.
|
|
340
|
+
leaked_id = re.fullmatch(r"(?P<name>.+?)_id[a-z0-9]+", requested_name, re.IGNORECASE)
|
|
341
|
+
if leaked_id:
|
|
342
|
+
prefix = re.sub(r"[^a-z0-9]", "", leaked_id.group("name").lower())
|
|
343
|
+
return declared.get(prefix) or declared.get(_SAFE_TOOL_NAME_ALIASES.get(prefix, ""))
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _canonical_declared_argument_name(
|
|
348
|
+
requested_name: str,
|
|
349
|
+
canonical_tool_name: str,
|
|
350
|
+
declared_properties: frozenset[str],
|
|
351
|
+
) -> str | None:
|
|
352
|
+
if requested_name in declared_properties:
|
|
353
|
+
return requested_name
|
|
354
|
+
key = re.sub(r"[^a-z0-9]", "", requested_name.lower())
|
|
355
|
+
for declared_name in declared_properties:
|
|
356
|
+
if re.sub(r"[^a-z0-9]", "", declared_name.lower()) == key:
|
|
357
|
+
return declared_name
|
|
358
|
+
alias = _SAFE_TOOL_ARGUMENT_ALIASES.get(canonical_tool_name, {}).get(key)
|
|
359
|
+
return alias if alias in declared_properties else None
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _declared_tool_properties(
|
|
363
|
+
tools: list[dict[str, Any]] | None,
|
|
364
|
+
) -> dict[str, frozenset[str]]:
|
|
365
|
+
"""Return declared JSON argument names keyed by canonical tool name.
|
|
366
|
+
|
|
367
|
+
A few OpenAI-compatible GLM gateways have emitted a native function-call
|
|
368
|
+
JSON key such as ``task</arg_key>``. We only repair that wire residue when
|
|
369
|
+
the cleaned name is an explicitly declared property; arbitrary model keys
|
|
370
|
+
are never guessed or renamed.
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
declared: dict[str, frozenset[str]] = {}
|
|
374
|
+
for tool in tools or []:
|
|
375
|
+
function = tool.get("function") or tool
|
|
376
|
+
name = str(function.get("name") or "").strip()
|
|
377
|
+
parameters = function.get("parameters") or {}
|
|
378
|
+
properties = parameters.get("properties") if isinstance(parameters, dict) else {}
|
|
379
|
+
if name:
|
|
380
|
+
declared[name] = (
|
|
381
|
+
frozenset(str(key) for key in properties if isinstance(key, str))
|
|
382
|
+
if isinstance(properties, dict)
|
|
383
|
+
else frozenset()
|
|
384
|
+
)
|
|
385
|
+
return declared
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _normalize_tool_call_arguments(
|
|
389
|
+
calls: tuple[ToolCall, ...] | list[ToolCall],
|
|
390
|
+
tools: list[dict[str, Any]] | None,
|
|
391
|
+
) -> tuple[ToolCall, ...]:
|
|
392
|
+
"""Normalize known provider wire residue in native function arguments."""
|
|
393
|
+
|
|
394
|
+
properties_by_tool = _declared_tool_properties(tools)
|
|
395
|
+
declared_names = _declared_tool_names(tools)
|
|
396
|
+
normalized_calls: list[ToolCall] = []
|
|
397
|
+
for call in calls:
|
|
398
|
+
canonical_name = _canonical_declared_tool_name(call.name, declared_names) or call.name
|
|
399
|
+
properties = properties_by_tool.get(canonical_name, frozenset())
|
|
400
|
+
if not properties:
|
|
401
|
+
normalized_calls.append(
|
|
402
|
+
ToolCall(
|
|
403
|
+
id=call.id,
|
|
404
|
+
name=canonical_name,
|
|
405
|
+
arguments=call.arguments,
|
|
406
|
+
raw=call.raw,
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
continue
|
|
99
410
|
try:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
411
|
+
arguments = load_model_json(call.arguments or "{}")
|
|
412
|
+
except (TypeError, ValueError):
|
|
413
|
+
normalized_calls.append(call)
|
|
414
|
+
continue
|
|
415
|
+
if not isinstance(arguments, dict):
|
|
416
|
+
normalized_calls.append(call)
|
|
417
|
+
continue
|
|
418
|
+
repaired: dict[str, Any] = {}
|
|
419
|
+
changed = False
|
|
420
|
+
for raw_key, value in arguments.items():
|
|
421
|
+
key = str(raw_key)
|
|
422
|
+
candidate = _GLM_ARGUMENT_KEY_RESIDUE_RE.sub("", html.unescape(key).strip()).strip()
|
|
423
|
+
if key not in properties and candidate in properties:
|
|
424
|
+
key = candidate
|
|
425
|
+
changed = True
|
|
426
|
+
if key in repaired:
|
|
427
|
+
raise StudioError(
|
|
428
|
+
"MODEL_TOOL_PROTOCOL_INVALID",
|
|
429
|
+
"模型返回了重复的工具参数",
|
|
430
|
+
status_code=502,
|
|
431
|
+
details={"toolName": call.name, "argument": key},
|
|
432
|
+
)
|
|
433
|
+
repaired[key] = value
|
|
434
|
+
normalized_calls.append(
|
|
435
|
+
ToolCall(
|
|
436
|
+
id=call.id,
|
|
437
|
+
name=canonical_name,
|
|
438
|
+
arguments=(json.dumps(repaired, ensure_ascii=False) if changed else call.arguments),
|
|
439
|
+
raw=call.raw,
|
|
440
|
+
)
|
|
441
|
+
)
|
|
442
|
+
return tuple(normalized_calls)
|
|
108
443
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
444
|
+
|
|
445
|
+
class _AmbiguousToolInput(StudioError):
|
|
446
|
+
"""A failed identity/value check cannot enter malformed-text recovery."""
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _textual_argument_error() -> _AmbiguousToolInput:
|
|
450
|
+
return _AmbiguousToolInput(
|
|
451
|
+
"MODEL_TOOL_PROTOCOL_INVALID", "模型返回了重复或非法的工具参数", status_code=502,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _store_textual_argument(arguments: dict[str, Any], name: str, value: Any) -> None:
|
|
456
|
+
if name in arguments:
|
|
457
|
+
raise _textual_argument_error()
|
|
458
|
+
arguments[name] = value
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _dsml_argument_value(raw: str, attributes: dict[str, str]) -> Any:
|
|
462
|
+
value = html.unescape(raw.strip())
|
|
463
|
+
if attributes.get("string", "").lower() == "true":
|
|
464
|
+
return value
|
|
465
|
+
try:
|
|
466
|
+
return load_model_json(value)
|
|
467
|
+
except ModelJSONPolicyError:
|
|
468
|
+
raise _textual_argument_error() from None
|
|
469
|
+
except (TypeError, ValueError):
|
|
470
|
+
return value
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _recover_dsml_tool_calls(
|
|
474
|
+
content: str,
|
|
475
|
+
tools: list[dict[str, Any]] | None,
|
|
476
|
+
) -> tuple[str, list[ToolCall], bool]:
|
|
477
|
+
"""Recover DeepSeek's textual DSML tool protocol without exposing it as prose."""
|
|
478
|
+
|
|
479
|
+
normalized = content.replace("|", "|")
|
|
480
|
+
if "DSML" not in normalized.upper():
|
|
481
|
+
return content, [], False
|
|
482
|
+
declared = _declared_tool_names(tools)
|
|
483
|
+
calls: list[ToolCall] = []
|
|
484
|
+
spans: list[tuple[int, int]] = []
|
|
485
|
+
for index, match in enumerate(_DSML_INVOKE_RE.finditer(normalized)):
|
|
486
|
+
attrs = _dsml_attributes(match.group("attrs"))
|
|
487
|
+
requested_name = str(attrs.get("name") or "").strip()
|
|
488
|
+
canonical_name = _canonical_declared_tool_name(requested_name, declared)
|
|
489
|
+
if not canonical_name:
|
|
113
490
|
raise StudioError(
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
status_code=
|
|
117
|
-
|
|
491
|
+
"MODEL_TOOL_PROTOCOL_INVALID",
|
|
492
|
+
f"模型返回了未声明的工具调用:{requested_name or 'unknown'}",
|
|
493
|
+
status_code=502,
|
|
494
|
+
details={"toolName": requested_name or "unknown"},
|
|
495
|
+
)
|
|
496
|
+
body = match.group("body")
|
|
497
|
+
arguments: dict[str, Any] = {}
|
|
498
|
+
for parameter in _DSML_PARAMETER_RE.finditer(body):
|
|
499
|
+
parameter_attrs = _dsml_attributes(parameter.group("attrs"))
|
|
500
|
+
parameter_name = str(parameter_attrs.get("name") or "").strip()
|
|
501
|
+
if parameter_name:
|
|
502
|
+
_store_textual_argument(
|
|
503
|
+
arguments, parameter_name,
|
|
504
|
+
_dsml_argument_value(parameter.group("value"), parameter_attrs),
|
|
505
|
+
)
|
|
506
|
+
if not arguments:
|
|
507
|
+
for parameter in _DSML_SIMPLE_ARGUMENT_RE.finditer(body):
|
|
508
|
+
parameter_name = parameter.group("name")
|
|
509
|
+
_store_textual_argument(arguments, parameter_name,
|
|
510
|
+
_dsml_argument_value(parameter.group("value"), {}))
|
|
511
|
+
digest = hashlib.sha256(f"{canonical_name}:{index}:{match.group(0)}".encode()).hexdigest()[
|
|
512
|
+
:20
|
|
513
|
+
]
|
|
514
|
+
calls.append(
|
|
515
|
+
ToolCall(
|
|
516
|
+
id=f"call_dsml_{digest}",
|
|
517
|
+
name=canonical_name,
|
|
518
|
+
arguments=json.dumps(arguments, ensure_ascii=False),
|
|
519
|
+
raw={"protocol": "dsml"},
|
|
118
520
|
)
|
|
119
|
-
|
|
521
|
+
)
|
|
522
|
+
spans.append(match.span())
|
|
523
|
+
if not calls:
|
|
524
|
+
raise StudioError(
|
|
525
|
+
"MODEL_TOOL_PROTOCOL_INVALID",
|
|
526
|
+
"模型返回了无法解析的工具调用",
|
|
527
|
+
status_code=502,
|
|
528
|
+
)
|
|
529
|
+
cleaned = normalized
|
|
530
|
+
for start, end in reversed(spans):
|
|
531
|
+
cleaned = cleaned[:start] + cleaned[end:]
|
|
532
|
+
cleaned = _DSML_CALLS_WRAPPER_RE.sub("", cleaned).strip()
|
|
533
|
+
if "DSML" in cleaned.upper():
|
|
534
|
+
raise StudioError(
|
|
535
|
+
"MODEL_TOOL_PROTOCOL_INVALID",
|
|
536
|
+
"模型返回了不完整的工具调用",
|
|
537
|
+
status_code=502,
|
|
538
|
+
)
|
|
539
|
+
return cleaned, calls, True
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _recover_glm_tool_calls(
|
|
543
|
+
content: str,
|
|
544
|
+
tools: list[dict[str, Any]] | None,
|
|
545
|
+
) -> tuple[str, list[ToolCall], bool]:
|
|
546
|
+
"""Recover GLM's textual ``<tool_call>`` fallback protocol.
|
|
120
547
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
548
|
+
Some OpenAI-compatible GLM endpoints put tool calls in ``content`` rather
|
|
549
|
+
than ``delta.tool_calls``. Real responses also occasionally encode an
|
|
550
|
+
argument as ``<arg_value>name=\"value\"</arg_value>`` (or close an
|
|
551
|
+
``arg_key`` with ``</arg_value>``), so parsing is intentionally tolerant
|
|
552
|
+
about those two wire quirks while remaining fail-closed on tool names.
|
|
553
|
+
"""
|
|
554
|
+
|
|
555
|
+
if "<tool_call" not in content.lower():
|
|
556
|
+
return content, [], False
|
|
557
|
+
declared = _declared_tool_names(tools)
|
|
558
|
+
calls: list[ToolCall] = []
|
|
559
|
+
spans: list[tuple[int, int]] = []
|
|
560
|
+
for index, match in enumerate(_GLM_TOOL_CALL_RE.finditer(content)):
|
|
561
|
+
requested_name = match.group("name").strip()
|
|
562
|
+
canonical_name = _canonical_declared_tool_name(requested_name, declared)
|
|
563
|
+
if not canonical_name:
|
|
124
564
|
raise StudioError(
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
status_code=
|
|
128
|
-
details={"
|
|
565
|
+
"MODEL_TOOL_PROTOCOL_INVALID",
|
|
566
|
+
f"模型返回了未声明的工具调用:{requested_name or 'unknown'}",
|
|
567
|
+
status_code=502,
|
|
568
|
+
details={"toolName": requested_name or "unknown"},
|
|
129
569
|
)
|
|
130
|
-
|
|
570
|
+
arguments: dict[str, Any] = {}
|
|
571
|
+
pending_key = ""
|
|
572
|
+
for token in _GLM_ARGUMENT_TAG_RE.finditer(match.group("body")):
|
|
573
|
+
tag = token.group("tag").lower()
|
|
574
|
+
raw_value = html.unescape(token.group("value").strip())
|
|
575
|
+
assignment = _GLM_ASSIGNMENT_RE.match(raw_value)
|
|
576
|
+
if assignment:
|
|
577
|
+
_store_textual_argument(
|
|
578
|
+
arguments, assignment.group("name"),
|
|
579
|
+
_dsml_argument_value(assignment.group("value"), {"string": "true"}),
|
|
580
|
+
)
|
|
581
|
+
pending_key = ""
|
|
582
|
+
elif tag == "arg_key":
|
|
583
|
+
pending_key = raw_value
|
|
584
|
+
elif pending_key:
|
|
585
|
+
_store_textual_argument(arguments, pending_key, _dsml_argument_value(raw_value, {}))
|
|
586
|
+
pending_key = ""
|
|
587
|
+
else:
|
|
588
|
+
raise StudioError(
|
|
589
|
+
"MODEL_TOOL_PROTOCOL_INVALID",
|
|
590
|
+
"模型返回了无法解析的工具参数",
|
|
591
|
+
status_code=502,
|
|
592
|
+
)
|
|
593
|
+
if pending_key or not arguments:
|
|
594
|
+
raise StudioError(
|
|
595
|
+
"MODEL_TOOL_PROTOCOL_INVALID",
|
|
596
|
+
"模型返回了不完整的工具调用",
|
|
597
|
+
status_code=502,
|
|
598
|
+
)
|
|
599
|
+
digest = hashlib.sha256(f"{canonical_name}:{index}:{match.group(0)}".encode()).hexdigest()[
|
|
600
|
+
:20
|
|
601
|
+
]
|
|
602
|
+
calls.append(
|
|
603
|
+
ToolCall(
|
|
604
|
+
id=f"call_glm_{digest}",
|
|
605
|
+
name=canonical_name,
|
|
606
|
+
arguments=json.dumps(arguments, ensure_ascii=False),
|
|
607
|
+
raw={"protocol": "glm-text"},
|
|
608
|
+
)
|
|
609
|
+
)
|
|
610
|
+
spans.append(match.span())
|
|
611
|
+
if not calls:
|
|
612
|
+
raise StudioError(
|
|
613
|
+
"MODEL_TOOL_PROTOCOL_INVALID",
|
|
614
|
+
"模型返回了无法解析的工具调用",
|
|
615
|
+
status_code=502,
|
|
616
|
+
)
|
|
617
|
+
cleaned = content
|
|
618
|
+
for start, end in reversed(spans):
|
|
619
|
+
cleaned = cleaned[:start] + cleaned[end:]
|
|
620
|
+
if "<tool_call" in cleaned.lower():
|
|
621
|
+
raise StudioError(
|
|
622
|
+
"MODEL_TOOL_PROTOCOL_INVALID",
|
|
623
|
+
"模型返回了不完整的工具调用",
|
|
624
|
+
status_code=502,
|
|
625
|
+
)
|
|
626
|
+
return cleaned.strip(), calls, True
|
|
131
627
|
|
|
132
|
-
@staticmethod
|
|
133
|
-
def _zero(value: bytearray | None) -> None:
|
|
134
|
-
if value is not None:
|
|
135
|
-
value[:] = b"\x00" * len(value)
|
|
136
628
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
629
|
+
def _recover_malformed_glm_tool_call(
|
|
630
|
+
content: str,
|
|
631
|
+
tools: list[dict[str, Any]] | None,
|
|
632
|
+
) -> tuple[str, list[ToolCall], bool]:
|
|
633
|
+
"""Recover the bounded malformed fallback emitted by GLM 5.2.
|
|
634
|
+
|
|
635
|
+
Some GLM 5.2 compatible gateways emit a textual ``<tool_call>`` marker
|
|
636
|
+
but lose the closing marker and mix up ``arg_key``/``arg_value`` tags.
|
|
637
|
+
Recovery remains fail-closed: the tool must resolve to a declared schema,
|
|
638
|
+
every recovered argument must resolve to a declared property, and at
|
|
639
|
+
least one argument must be recovered. Text after the marker is discarded
|
|
640
|
+
so a hallucinated "file created" sentence cannot escape as visible output
|
|
641
|
+
before the tool actually executes.
|
|
642
|
+
"""
|
|
643
|
+
|
|
644
|
+
start = _GLM_MALFORMED_TOOL_START_RE.search(content)
|
|
645
|
+
if start is None:
|
|
646
|
+
return content, [], False
|
|
647
|
+
declared_names = _declared_tool_names(tools)
|
|
648
|
+
requested_name = start.group("name").strip()
|
|
649
|
+
canonical_name = _canonical_declared_tool_name(requested_name, declared_names)
|
|
650
|
+
if not canonical_name:
|
|
651
|
+
raise StudioError(
|
|
652
|
+
"MODEL_TOOL_PROTOCOL_INVALID",
|
|
653
|
+
f"模型返回了未声明的工具调用:{requested_name or 'unknown'}",
|
|
654
|
+
status_code=502,
|
|
655
|
+
details={"toolName": requested_name or "unknown"},
|
|
656
|
+
)
|
|
657
|
+
properties = _declared_tool_properties(tools).get(canonical_name, frozenset())
|
|
658
|
+
body = content[start.end() :]
|
|
659
|
+
raw_arguments: list[tuple[str, str]] = []
|
|
660
|
+
|
|
661
|
+
key_matches = list(_GLM_MALFORMED_KEY_RE.finditer(body))
|
|
662
|
+
for index, key_match in enumerate(key_matches):
|
|
663
|
+
next_start = key_matches[index + 1].start() if index + 1 < len(key_matches) else len(body)
|
|
664
|
+
value = body[key_match.end() : next_start]
|
|
665
|
+
value = re.sub(r"^\s*<\s*arg_value\s*>\s*", "", value, flags=re.IGNORECASE)
|
|
666
|
+
value = re.split(r"</\s*arg_value\s*>", value, maxsplit=1, flags=re.IGNORECASE)[0]
|
|
667
|
+
raw_arguments.append((key_match.group("name"), html.unescape(value.strip())))
|
|
668
|
+
|
|
669
|
+
if not raw_arguments:
|
|
670
|
+
raw_arguments.extend(
|
|
671
|
+
(match.group("name"), html.unescape(match.group("value")))
|
|
672
|
+
for match in _GLM_MALFORMED_ASSIGNMENT_RE.finditer(body)
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
arguments: dict[str, Any] = {}
|
|
676
|
+
for raw_name, raw_value in raw_arguments:
|
|
677
|
+
argument_name = _canonical_declared_argument_name(
|
|
678
|
+
raw_name,
|
|
679
|
+
canonical_name,
|
|
680
|
+
properties,
|
|
681
|
+
)
|
|
682
|
+
if not argument_name:
|
|
145
683
|
raise StudioError(
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
status_code=
|
|
149
|
-
|
|
684
|
+
"MODEL_TOOL_PROTOCOL_INVALID",
|
|
685
|
+
"模型返回了未声明的工具参数",
|
|
686
|
+
status_code=502,
|
|
687
|
+
details={"toolName": canonical_name, "argument": raw_name},
|
|
150
688
|
)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
persisted[name] = value
|
|
160
|
-
self._write_persisted()
|
|
161
|
-
return self.status(f"env://{name}")
|
|
162
|
-
|
|
163
|
-
def delete_session(self, name: str) -> dict[str, str | bool]:
|
|
164
|
-
name = self._validate_name(name)
|
|
165
|
-
with self._lock:
|
|
166
|
-
previous = self._session_values.pop(name, None)
|
|
167
|
-
self._zero(previous)
|
|
168
|
-
if self._secrets_path() is not None:
|
|
169
|
-
persisted = self._load_persisted()
|
|
170
|
-
if name in persisted:
|
|
171
|
-
persisted.pop(name, None)
|
|
172
|
-
self._write_persisted()
|
|
173
|
-
return self.status(f"env://{name}")
|
|
174
|
-
|
|
175
|
-
def clear_session(self) -> None:
|
|
176
|
-
with self._lock:
|
|
177
|
-
values = list(self._session_values.values())
|
|
178
|
-
self._session_values.clear()
|
|
179
|
-
for value in values:
|
|
180
|
-
self._zero(value)
|
|
181
|
-
|
|
182
|
-
def _fallback_name(self, name: str) -> str | None:
|
|
183
|
-
for primary, alternate in self._FALLBACK_PAIRS:
|
|
184
|
-
if name == primary and alternate != primary:
|
|
185
|
-
return alternate
|
|
186
|
-
return None
|
|
187
|
-
|
|
188
|
-
def _resolve_source(self, name: str) -> tuple[bool, str]:
|
|
189
|
-
"""Return (configured, source) considering session, persisted file, env, and fallback."""
|
|
190
|
-
fallback = self._fallback_name(name)
|
|
191
|
-
with self._lock:
|
|
192
|
-
if name in self._session_values:
|
|
193
|
-
return True, "session"
|
|
194
|
-
if fallback and fallback in self._session_values:
|
|
195
|
-
return True, "session-alias"
|
|
196
|
-
persisted = self._load_persisted()
|
|
197
|
-
if name in persisted:
|
|
198
|
-
return True, "workspace"
|
|
199
|
-
if fallback and fallback in persisted:
|
|
200
|
-
return True, "workspace-alias"
|
|
201
|
-
if os.environ.get(name):
|
|
202
|
-
return True, "environment"
|
|
203
|
-
if fallback and os.environ.get(fallback):
|
|
204
|
-
return True, "fallback"
|
|
205
|
-
return False, "missing"
|
|
206
|
-
|
|
207
|
-
def _session_value(self, name: str) -> str | None:
|
|
208
|
-
with self._lock:
|
|
209
|
-
value = self._session_values.get(name)
|
|
210
|
-
return value.decode("utf-8") if value is not None else None
|
|
211
|
-
|
|
212
|
-
def status(self, reference: str) -> dict[str, str | bool]:
|
|
213
|
-
name = self._environment_name(reference)
|
|
214
|
-
configured, source = self._resolve_source(name)
|
|
215
|
-
return {
|
|
216
|
-
"reference": reference,
|
|
217
|
-
"name": name,
|
|
218
|
-
"configured": configured,
|
|
219
|
-
"source": source,
|
|
220
|
-
"persistence": source,
|
|
221
|
-
}
|
|
689
|
+
if argument_name in arguments:
|
|
690
|
+
raise StudioError(
|
|
691
|
+
"MODEL_TOOL_PROTOCOL_INVALID",
|
|
692
|
+
"模型返回了重复的工具参数",
|
|
693
|
+
status_code=502,
|
|
694
|
+
details={"toolName": canonical_name, "argument": argument_name},
|
|
695
|
+
)
|
|
696
|
+
arguments[argument_name] = _dsml_argument_value(raw_value, {"string": "true"})
|
|
222
697
|
|
|
223
|
-
|
|
224
|
-
name = self._environment_name(reference)
|
|
225
|
-
fallback = self._fallback_name(name) if allow_aliases else None
|
|
226
|
-
aliases = [name, *([fallback] if fallback else [])]
|
|
227
|
-
for candidate in aliases:
|
|
228
|
-
value = self._session_value(candidate)
|
|
229
|
-
if value is not None:
|
|
230
|
-
return value
|
|
231
|
-
persisted = self._load_persisted()
|
|
232
|
-
for candidate in aliases:
|
|
233
|
-
if candidate in persisted:
|
|
234
|
-
return persisted[candidate]
|
|
235
|
-
for candidate in aliases:
|
|
236
|
-
value = os.environ.get(candidate)
|
|
237
|
-
if value:
|
|
238
|
-
return value
|
|
698
|
+
if not arguments:
|
|
239
699
|
raise StudioError(
|
|
240
|
-
"
|
|
241
|
-
"
|
|
242
|
-
status_code=
|
|
243
|
-
details={"reference": reference},
|
|
700
|
+
"MODEL_TOOL_PROTOCOL_INVALID",
|
|
701
|
+
"模型返回了无法解析的工具调用",
|
|
702
|
+
status_code=502,
|
|
244
703
|
)
|
|
704
|
+
digest = hashlib.sha256(
|
|
705
|
+
f"{canonical_name}:malformed:{content[start.start() :]}".encode()
|
|
706
|
+
).hexdigest()[:20]
|
|
707
|
+
return (
|
|
708
|
+
content[: start.start()].strip(),
|
|
709
|
+
[
|
|
710
|
+
ToolCall(
|
|
711
|
+
id=f"call_glm_{digest}",
|
|
712
|
+
name=canonical_name,
|
|
713
|
+
arguments=json.dumps(arguments, ensure_ascii=False),
|
|
714
|
+
raw={"protocol": "glm-text-recovered"},
|
|
715
|
+
)
|
|
716
|
+
],
|
|
717
|
+
True,
|
|
718
|
+
)
|
|
245
719
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
720
|
+
|
|
721
|
+
def _recover_textual_tool_calls(
|
|
722
|
+
content: str,
|
|
723
|
+
tools: list[dict[str, Any]] | None,
|
|
724
|
+
) -> tuple[str, list[ToolCall], bool]:
|
|
725
|
+
if "DSML" in content.replace("|", "|").upper():
|
|
726
|
+
return _recover_dsml_tool_calls(content, tools)
|
|
727
|
+
try:
|
|
728
|
+
return _recover_glm_tool_calls(content, tools)
|
|
729
|
+
except StudioError as exc:
|
|
730
|
+
if isinstance(exc, _AmbiguousToolInput):
|
|
731
|
+
raise
|
|
732
|
+
if exc.code != "MODEL_TOOL_PROTOCOL_INVALID":
|
|
733
|
+
raise
|
|
734
|
+
return _recover_malformed_glm_tool_call(content, tools)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _partition_tool_stream_text(value: str) -> tuple[str, str, bool]:
|
|
738
|
+
"""Split a streamed text buffer before a possible textual tool marker.
|
|
739
|
+
|
|
740
|
+
Ordinary model prose must remain truly streaming even when tools are
|
|
741
|
+
available. Only the short suffix beginning at ``<`` is held long enough
|
|
742
|
+
to decide whether a provider is starting a textual DSML tool call.
|
|
743
|
+
"""
|
|
744
|
+
|
|
745
|
+
normalized = value.replace("|", "|")
|
|
746
|
+
marker = re.search(
|
|
747
|
+
r"<\s*(?:\|\s*){1,2}DSML|<\s*tool_call\s*>",
|
|
748
|
+
normalized,
|
|
749
|
+
re.IGNORECASE,
|
|
750
|
+
)
|
|
751
|
+
if marker:
|
|
752
|
+
start = marker.start()
|
|
753
|
+
return value[:start], value[start:], True
|
|
754
|
+
candidate_start = value.rfind("<")
|
|
755
|
+
if candidate_start >= 0 and len(value) - candidate_start <= _DSML_STREAM_LOOKBEHIND:
|
|
756
|
+
return value[:candidate_start], value[candidate_start:], False
|
|
757
|
+
return value, "", False
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def _repeated_output_unit(value: str) -> str:
|
|
761
|
+
"""Return a repeated tail unit when a provider is stuck in a text loop."""
|
|
762
|
+
|
|
763
|
+
if len(value) < _REPETITION_MIN_UNIT_CHARS * _REPETITION_COUNT:
|
|
764
|
+
return ""
|
|
765
|
+
window = value[-(_REPETITION_MAX_UNIT_CHARS * _REPETITION_COUNT) :]
|
|
766
|
+
max_unit = min(_REPETITION_MAX_UNIT_CHARS, len(window) // _REPETITION_COUNT)
|
|
767
|
+
for unit_length in range(_REPETITION_MIN_UNIT_CHARS, max_unit + 1):
|
|
768
|
+
unit = window[-unit_length:]
|
|
769
|
+
if window.endswith(unit * _REPETITION_COUNT) and len(set(unit.strip())) >= 6:
|
|
770
|
+
return unit
|
|
771
|
+
return ""
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _raise_for_repetitive_output(value: str) -> None:
|
|
775
|
+
unit = _repeated_output_unit(value)
|
|
776
|
+
if not unit:
|
|
777
|
+
return
|
|
778
|
+
raise StudioError(
|
|
779
|
+
"MODEL_REPETITIVE_OUTPUT",
|
|
780
|
+
"模型输出出现重复循环,已停止本次生成",
|
|
781
|
+
status_code=502,
|
|
782
|
+
details={"repeatedChars": len(unit)},
|
|
783
|
+
)
|
|
251
784
|
|
|
252
785
|
|
|
253
786
|
class NetworkGuard:
|
|
@@ -319,6 +852,34 @@ class OpenAICompatibleModelClient:
|
|
|
319
852
|
self.network_guard = network_guard or NetworkGuard()
|
|
320
853
|
self.transport = transport
|
|
321
854
|
self.sleep = sleep
|
|
855
|
+
# Keep one AsyncClient for the lifetime of the Studio service. Creating
|
|
856
|
+
# a client for every turn defeats HTTP keep-alive and forces a fresh
|
|
857
|
+
# TCP/TLS handshake before the model can produce its first token.
|
|
858
|
+
self._client: httpx.AsyncClient | None = None
|
|
859
|
+
self._client_lock = RLock()
|
|
860
|
+
|
|
861
|
+
def _http_client(self) -> httpx.AsyncClient:
|
|
862
|
+
with self._client_lock:
|
|
863
|
+
if self._client is None or self._client.is_closed:
|
|
864
|
+
self._client = httpx.AsyncClient(
|
|
865
|
+
transport=self.transport,
|
|
866
|
+
follow_redirects=False,
|
|
867
|
+
limits=httpx.Limits(
|
|
868
|
+
max_connections=20,
|
|
869
|
+
max_keepalive_connections=10,
|
|
870
|
+
keepalive_expiry=30.0,
|
|
871
|
+
),
|
|
872
|
+
)
|
|
873
|
+
return self._client
|
|
874
|
+
|
|
875
|
+
async def aclose(self) -> None:
|
|
876
|
+
"""Close the shared connection pool during Studio shutdown."""
|
|
877
|
+
|
|
878
|
+
with self._client_lock:
|
|
879
|
+
client = self._client
|
|
880
|
+
self._client = None
|
|
881
|
+
if client is not None and not client.is_closed:
|
|
882
|
+
await client.aclose()
|
|
322
883
|
|
|
323
884
|
async def complete(
|
|
324
885
|
self,
|
|
@@ -326,20 +887,35 @@ class OpenAICompatibleModelClient:
|
|
|
326
887
|
*,
|
|
327
888
|
messages: list[dict[str, Any]],
|
|
328
889
|
network_policy: NetworkPolicy,
|
|
329
|
-
timeout_seconds:
|
|
890
|
+
timeout_seconds: float,
|
|
330
891
|
max_attempts: int,
|
|
331
892
|
backoff_seconds: float,
|
|
332
893
|
tools: list[dict[str, Any]] | None = None,
|
|
333
894
|
allow_empty: bool = False,
|
|
334
895
|
response_format: dict[str, Any] | None = None,
|
|
335
896
|
retry_on_length: bool = False,
|
|
897
|
+
retry_reasoning_only: bool = True,
|
|
898
|
+
max_output_tokens: int | None = None,
|
|
899
|
+
output_budget_mode: Literal["adaptive", "fixed"] = "adaptive",
|
|
900
|
+
reasoning_effort: str | None = None,
|
|
901
|
+
tool_choice: str | dict[str, Any] | None = None,
|
|
902
|
+
parallel_tool_calls: bool | None = None,
|
|
903
|
+
output_token_parameter: Literal["max_tokens", "max_completion_tokens"] | None = None,
|
|
904
|
+
compatibility_profile: provider_compat.CompatProfile | None = None,
|
|
905
|
+
thinking_mode: Literal["disabled"] | None = None,
|
|
336
906
|
) -> ModelResponse:
|
|
337
907
|
await self.network_guard.check(model.endpoint_url, network_policy)
|
|
338
908
|
credential = self.credential_resolver.resolve(model.credential_ref)
|
|
339
909
|
wire_api = (model.wire_api or "chat").strip().lower()
|
|
910
|
+
# 端点差异只允许来自声明式兼容档位(不按模型名写传输/解析分支)。
|
|
911
|
+
profile = compatibility_profile or provider_compat.compat_profile(
|
|
912
|
+
base_url=model.endpoint_url, model=model.model
|
|
913
|
+
)
|
|
340
914
|
payload: dict[str, Any]
|
|
341
915
|
if wire_api == "responses":
|
|
342
916
|
payload = self._responses_payload(model, messages, tools)
|
|
917
|
+
elif wire_api == "anthropic":
|
|
918
|
+
payload = self._anthropic_payload(model, messages, tools, compatibility_profile=profile)
|
|
343
919
|
else:
|
|
344
920
|
payload = {
|
|
345
921
|
"model": model.model,
|
|
@@ -356,30 +932,75 @@ class OpenAICompatibleModelClient:
|
|
|
356
932
|
payload["top_p"] = model.parameters.top_p
|
|
357
933
|
if tools:
|
|
358
934
|
payload["tools"] = tools
|
|
359
|
-
payload["tool_choice"] = "auto"
|
|
935
|
+
payload["tool_choice"] = tool_choice or "auto"
|
|
936
|
+
# OpenAI-compatible gateways do not all share the same default.
|
|
937
|
+
# Declare this explicitly so one model turn may return several
|
|
938
|
+
# independent calls; the stream parser and Harness dispatcher
|
|
939
|
+
# already preserve and execute such batches.
|
|
940
|
+
payload["parallel_tool_calls"] = True
|
|
360
941
|
if response_format and model.parameters.allow_json_response_format:
|
|
361
942
|
payload["response_format"] = response_format
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
943
|
+
payload = provider_compat.apply_chat_compat(payload, profile, model=model.model)
|
|
944
|
+
self._apply_tool_choice(payload, wire_api, tool_choice)
|
|
945
|
+
self._apply_parallel_tool_policy(payload, wire_api, parallel_tool_calls)
|
|
946
|
+
if max_output_tokens is not None:
|
|
947
|
+
payload["max_output_tokens" if wire_api == "responses" else "max_tokens"] = (
|
|
948
|
+
max_output_tokens
|
|
949
|
+
)
|
|
950
|
+
# A Native call owns a hard output limit. Endpoint recommendations
|
|
951
|
+
# and legacy length recovery must not enlarge that allocation.
|
|
952
|
+
if (
|
|
953
|
+
output_budget_mode == "adaptive"
|
|
954
|
+
and wire_api != "responses" and max_output_tokens is not None
|
|
955
|
+
):
|
|
956
|
+
# 不变量:max_output_tokens > thinking_budget。只在**引擎显式压预算**
|
|
957
|
+
# (委派决策回合)时抬:这是实测零产出事故的成因;agent 作者自己配置
|
|
958
|
+
# 的 maxTokens 不静默改写,否则等于替作者改配置。
|
|
959
|
+
effective_budget = provider_compat.required_output_budget(
|
|
960
|
+
profile, payload.get("max_tokens")
|
|
961
|
+
)
|
|
962
|
+
if effective_budget is not None:
|
|
963
|
+
payload["max_tokens"] = effective_budget
|
|
964
|
+
effective_effort = _effective_reasoning_effort(model, tools, reasoning_effort, profile)
|
|
965
|
+
self._apply_reasoning_effort(payload, wire_api, effective_effort)
|
|
966
|
+
# 思考决策必须在"有效输出预算"确定之后做,且**与是否带工具无关**:
|
|
967
|
+
# 预算不足以容纳思考预算时(如委派决策回合的 2048),必须关思考,
|
|
968
|
+
# 否则始终思考的模型会把预算全花在 reasoning 上、零产出。
|
|
969
|
+
self._apply_thinking_policy(payload, wire_api, profile, tools, effective_effort,
|
|
970
|
+
thinking_mode=thinking_mode)
|
|
971
|
+
if wire_api not in {"responses", "anthropic"}:
|
|
972
|
+
# Apply the complete endpoint/model policy after all host options,
|
|
973
|
+
# so a late effort or thinking switch cannot reintroduce a field
|
|
974
|
+
# rejected by this model. No adaptive retry or budget increase.
|
|
975
|
+
payload = provider_compat.apply_chat_compat(payload, profile, model=model.model)
|
|
976
|
+
self._apply_output_token_parameter(payload, wire_api, output_token_parameter)
|
|
977
|
+
if wire_api == "anthropic":
|
|
978
|
+
headers = self._anthropic_headers(credential, payload)
|
|
979
|
+
else:
|
|
980
|
+
headers = {
|
|
981
|
+
"Authorization": f"Bearer {credential}",
|
|
982
|
+
"Content-Type": "application/json",
|
|
983
|
+
}
|
|
366
984
|
timeout = httpx.Timeout(timeout_seconds, connect=min(10, timeout_seconds))
|
|
367
985
|
dropped_response_format = False
|
|
368
986
|
length_retried = False
|
|
987
|
+
reasoning_only_retried = False
|
|
369
988
|
last_length_error: StudioError | None = None
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
) as client:
|
|
989
|
+
client = self._http_client()
|
|
990
|
+
observation = None
|
|
991
|
+
observed_usage = None
|
|
992
|
+
try:
|
|
375
993
|
# 额外 1 次迭代仅用于 response_format 400 降级重发,
|
|
376
994
|
# 其余失败路径仍受 max_attempts 约束(会在原上限处 raise)。
|
|
377
995
|
for attempt in range(1, max_attempts + 2):
|
|
996
|
+
observation = None
|
|
997
|
+
observed_usage = None
|
|
378
998
|
try:
|
|
379
999
|
response = await client.post(
|
|
380
1000
|
model.endpoint_url,
|
|
381
1001
|
headers=headers,
|
|
382
1002
|
json=payload,
|
|
1003
|
+
timeout=timeout,
|
|
383
1004
|
)
|
|
384
1005
|
except (httpx.TimeoutException, httpx.TransportError) as exc:
|
|
385
1006
|
if attempt >= max_attempts:
|
|
@@ -398,62 +1019,56 @@ class OpenAICompatibleModelClient:
|
|
|
398
1019
|
status_code=403,
|
|
399
1020
|
details={"statusCode": response.status_code},
|
|
400
1021
|
)
|
|
401
|
-
if response.status_code == 429 or response.status_code >= 500:
|
|
402
|
-
if attempt < max_attempts:
|
|
403
|
-
await self.sleep(backoff_seconds * attempt)
|
|
404
|
-
continue
|
|
405
1022
|
if (
|
|
406
1023
|
response.status_code == 400
|
|
407
1024
|
and "response_format" in payload
|
|
408
1025
|
and not dropped_response_format
|
|
409
1026
|
):
|
|
410
|
-
# 网关不支持 response_format:去掉该字段重发一次。
|
|
411
1027
|
dropped_response_format = True
|
|
412
1028
|
payload.pop("response_format")
|
|
413
1029
|
continue
|
|
414
1030
|
if response.status_code >= 400:
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
details={"upstreamStatus": response.status_code},
|
|
426
|
-
)
|
|
427
|
-
upstream_detail = ""
|
|
1031
|
+
failure = await _http_model_error(response, wire_api=wire_api)
|
|
1032
|
+
from ksadk.harness.model_provider import classify_model_failure
|
|
1033
|
+
|
|
1034
|
+
if (classify_model_failure(failure).kind.value in {"rate_limit", "unavailable"}
|
|
1035
|
+
and attempt < max_attempts):
|
|
1036
|
+
await self.sleep(backoff_seconds * attempt)
|
|
1037
|
+
continue
|
|
1038
|
+
raise failure
|
|
1039
|
+
observation = None
|
|
1040
|
+
if wire_api == "anthropic" and len(response.content) <= 16 * 1024 * 1024:
|
|
428
1041
|
try:
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
"MODEL_REQUEST_FAILED",
|
|
434
|
-
"模型服务返回错误",
|
|
435
|
-
status_code=502,
|
|
436
|
-
details={
|
|
437
|
-
"upstreamStatus": response.status_code,
|
|
438
|
-
"upstreamError": upstream_detail,
|
|
439
|
-
},
|
|
440
|
-
)
|
|
1042
|
+
observation = input_observation(decode_model_frame(response.content),
|
|
1043
|
+
payload, "buffered")
|
|
1044
|
+
except (ValueError, TypeError):
|
|
1045
|
+
raise invalid_model_frame() from None
|
|
441
1046
|
try:
|
|
442
1047
|
if wire_api == "responses":
|
|
443
1048
|
parsed = self._parse_responses_response(response, allow_empty=allow_empty)
|
|
1049
|
+
elif wire_api == "anthropic":
|
|
1050
|
+
parsed = self._parse_anthropic_response(response, allow_empty=allow_empty)
|
|
444
1051
|
else:
|
|
445
1052
|
parsed = self._parse_response(response, allow_empty=allow_empty)
|
|
446
1053
|
except StudioError as exc:
|
|
1054
|
+
if type(exc) is ModelResponseError:
|
|
1055
|
+
exc.provider_input = observation
|
|
447
1056
|
# finishReason=length 的空响应(大 JSON 被 max_tokens 截断):
|
|
448
1057
|
# 一次性扩容 max_tokens 重发;再次截断则按原错误上抛。
|
|
449
|
-
if
|
|
1058
|
+
if (
|
|
1059
|
+
retry_on_length and output_budget_mode == "adaptive"
|
|
1060
|
+
and not length_retried and self._is_length_truncation(exc)
|
|
1061
|
+
):
|
|
450
1062
|
length_retried = True
|
|
451
1063
|
last_length_error = exc
|
|
452
1064
|
payload = self._expand_length_budget(payload)
|
|
453
1065
|
continue
|
|
454
1066
|
raise
|
|
1067
|
+
parsed = replace(parsed, provider_input=observation)
|
|
1068
|
+
observed_usage = parsed.usage
|
|
455
1069
|
if (
|
|
456
1070
|
retry_on_length
|
|
1071
|
+
and output_budget_mode == "adaptive"
|
|
457
1072
|
and not length_retried
|
|
458
1073
|
and parsed.finish_reason in _LENGTH_FINISH_REASONS
|
|
459
1074
|
):
|
|
@@ -461,11 +1076,870 @@ class OpenAICompatibleModelClient:
|
|
|
461
1076
|
length_retried = True
|
|
462
1077
|
payload = self._expand_length_budget(payload)
|
|
463
1078
|
continue
|
|
1079
|
+
if parsed.finish_reason == "content_filter":
|
|
1080
|
+
return parsed
|
|
1081
|
+
if not (
|
|
1082
|
+
"DSML" in parsed.content.replace("|", "|").upper()
|
|
1083
|
+
or "<tool_call" in parsed.content.lower()
|
|
1084
|
+
):
|
|
1085
|
+
_raise_for_repetitive_output(parsed.content)
|
|
1086
|
+
if wire_api != "responses" and not parsed.tool_calls:
|
|
1087
|
+
try:
|
|
1088
|
+
cleaned, recovered_calls, recovered = _recover_textual_tool_calls(
|
|
1089
|
+
parsed.content, tools
|
|
1090
|
+
)
|
|
1091
|
+
except StudioError as error:
|
|
1092
|
+
raise ModelResponseError(error, parsed.usage) from None
|
|
1093
|
+
if recovered:
|
|
1094
|
+
parsed = ModelResponse(
|
|
1095
|
+
content=cleaned,
|
|
1096
|
+
finish_reason="tool_calls",
|
|
1097
|
+
usage=parsed.usage,
|
|
1098
|
+
tool_calls=recovered_calls,
|
|
1099
|
+
raw_message=parsed.raw_message,
|
|
1100
|
+
reasoning=parsed.reasoning,
|
|
1101
|
+
provider_request_id=parsed.provider_request_id,
|
|
1102
|
+
provider_input=parsed.provider_input,
|
|
1103
|
+
)
|
|
1104
|
+
if (
|
|
1105
|
+
wire_api != "responses"
|
|
1106
|
+
and tools
|
|
1107
|
+
and not parsed.content.strip()
|
|
1108
|
+
and not parsed.tool_calls
|
|
1109
|
+
and parsed.reasoning.strip()
|
|
1110
|
+
):
|
|
1111
|
+
# 思考型 OpenAI 兼容端点(GLM / DeepSeek / MiMo 等)在工具
|
|
1112
|
+
# 回合可能只返回 reasoning_content。这是**已知的正常形态**,
|
|
1113
|
+
# 不是传输故障:按档位关掉思考修一次(而不是强推
|
|
1114
|
+
# tool_choice=required),仍为空则作为"可见空回合"返回,
|
|
1115
|
+
# 交给上层循环决定——绝不升级成致命错误,否则会被 provider
|
|
1116
|
+
# 重试链放大成整轮 ABORT(2026-09-18 实测事故)。
|
|
1117
|
+
if retry_reasoning_only and not reasoning_only_retried:
|
|
1118
|
+
reasoning_only_retried = True
|
|
1119
|
+
repair_payload = {
|
|
1120
|
+
**payload,
|
|
1121
|
+
"messages": [
|
|
1122
|
+
*list(payload.get("messages") or []),
|
|
1123
|
+
{
|
|
1124
|
+
"role": "system",
|
|
1125
|
+
"content": (
|
|
1126
|
+
"The previous response contained reasoning only. "
|
|
1127
|
+
"Return a concrete final answer, or call one of the "
|
|
1128
|
+
"declared tools using the native tool-call protocol; "
|
|
1129
|
+
"do not return reasoning alone."
|
|
1130
|
+
),
|
|
1131
|
+
},
|
|
1132
|
+
],
|
|
1133
|
+
}
|
|
1134
|
+
payload = provider_compat.with_thinking_disabled(
|
|
1135
|
+
repair_payload, profile, reasoning_effort=effective_effort,
|
|
1136
|
+
)
|
|
1137
|
+
continue
|
|
1138
|
+
return ModelResponse(
|
|
1139
|
+
content="",
|
|
1140
|
+
finish_reason=parsed.finish_reason,
|
|
1141
|
+
usage=parsed.usage,
|
|
1142
|
+
tool_calls=[],
|
|
1143
|
+
raw_message=parsed.raw_message,
|
|
1144
|
+
reasoning=parsed.reasoning,
|
|
1145
|
+
empty_reasoning_only=True,
|
|
1146
|
+
provider_request_id=parsed.provider_request_id,
|
|
1147
|
+
provider_input=parsed.provider_input,
|
|
1148
|
+
)
|
|
1149
|
+
if parsed.tool_calls:
|
|
1150
|
+
try:
|
|
1151
|
+
normalized_calls = list(_normalize_tool_call_arguments(
|
|
1152
|
+
parsed.tool_calls, tools,
|
|
1153
|
+
))
|
|
1154
|
+
except StudioError as error:
|
|
1155
|
+
raise ModelResponseError(error, parsed.usage) from None
|
|
1156
|
+
parsed = ModelResponse(
|
|
1157
|
+
content=parsed.content,
|
|
1158
|
+
finish_reason=parsed.finish_reason,
|
|
1159
|
+
usage=parsed.usage,
|
|
1160
|
+
tool_calls=normalized_calls,
|
|
1161
|
+
raw_message=parsed.raw_message,
|
|
1162
|
+
reasoning=parsed.reasoning,
|
|
1163
|
+
provider_request_id=parsed.provider_request_id,
|
|
1164
|
+
provider_input=parsed.provider_input,
|
|
1165
|
+
)
|
|
464
1166
|
return parsed
|
|
1167
|
+
except asyncio.CancelledError:
|
|
1168
|
+
raise
|
|
1169
|
+
except StudioError as error:
|
|
1170
|
+
if observation is not None:
|
|
1171
|
+
failure = (error if type(error) is ModelResponseError else
|
|
1172
|
+
ModelResponseError(error, observed_usage))
|
|
1173
|
+
failure.provider_input = observation
|
|
1174
|
+
raise failure from None
|
|
1175
|
+
raise
|
|
465
1176
|
if last_length_error is not None:
|
|
466
1177
|
raise last_length_error
|
|
467
1178
|
raise AssertionError("unreachable")
|
|
468
1179
|
|
|
1180
|
+
async def stream(
|
|
1181
|
+
self,
|
|
1182
|
+
model: ResolvedModel,
|
|
1183
|
+
*,
|
|
1184
|
+
messages: list[dict[str, Any]],
|
|
1185
|
+
network_policy: NetworkPolicy,
|
|
1186
|
+
timeout_seconds: float,
|
|
1187
|
+
tools: list[dict[str, Any]] | None = None,
|
|
1188
|
+
max_output_tokens: int | None = None,
|
|
1189
|
+
output_budget_mode: Literal["adaptive", "fixed"] = "adaptive",
|
|
1190
|
+
reasoning_effort: str | None = None,
|
|
1191
|
+
retry_empty_stream: bool = True,
|
|
1192
|
+
tool_choice: str | dict[str, Any] | None = None,
|
|
1193
|
+
parallel_tool_calls: bool | None = None,
|
|
1194
|
+
output_token_parameter: Literal["max_tokens", "max_completion_tokens"] | None = None,
|
|
1195
|
+
compatibility_profile: provider_compat.CompatProfile | None = None,
|
|
1196
|
+
thinking_mode: Literal["disabled"] | None = None,
|
|
1197
|
+
) -> AsyncIterator[ModelStreamChunk]:
|
|
1198
|
+
"""Stream chat-completions deltas without buffering the response.
|
|
1199
|
+
|
|
1200
|
+
Studio's Harness path uses the same credential and network policy as
|
|
1201
|
+
``complete``. Wire-specific decoders share byte framing and lifecycle
|
|
1202
|
+
ownership; provider events remain outside the Native decision core.
|
|
1203
|
+
"""
|
|
1204
|
+
if (model.wire_api or "chat").strip().lower() == "responses":
|
|
1205
|
+
iterator = self._stream_responses(
|
|
1206
|
+
model,
|
|
1207
|
+
messages=messages,
|
|
1208
|
+
network_policy=network_policy,
|
|
1209
|
+
timeout_seconds=timeout_seconds,
|
|
1210
|
+
tools=tools,
|
|
1211
|
+
max_output_tokens=max_output_tokens,
|
|
1212
|
+
reasoning_effort=reasoning_effort,
|
|
1213
|
+
thinking_mode=thinking_mode,
|
|
1214
|
+
compatibility_profile=compatibility_profile,
|
|
1215
|
+
tool_choice=tool_choice,
|
|
1216
|
+
parallel_tool_calls=parallel_tool_calls,
|
|
1217
|
+
output_token_parameter=output_token_parameter,
|
|
1218
|
+
)
|
|
1219
|
+
async with owning_async_iterator(iterator, owner="studio.responses"):
|
|
1220
|
+
async for chunk in iterator:
|
|
1221
|
+
yield chunk
|
|
1222
|
+
return
|
|
1223
|
+
if (model.wire_api or "chat").strip().lower() == "anthropic":
|
|
1224
|
+
iterator = self._stream_anthropic(
|
|
1225
|
+
model,
|
|
1226
|
+
messages=messages,
|
|
1227
|
+
network_policy=network_policy,
|
|
1228
|
+
timeout_seconds=timeout_seconds,
|
|
1229
|
+
tools=tools,
|
|
1230
|
+
max_output_tokens=max_output_tokens,
|
|
1231
|
+
reasoning_effort=reasoning_effort,
|
|
1232
|
+
thinking_mode=thinking_mode,
|
|
1233
|
+
compatibility_profile=compatibility_profile,
|
|
1234
|
+
tool_choice=tool_choice,
|
|
1235
|
+
parallel_tool_calls=parallel_tool_calls,
|
|
1236
|
+
output_token_parameter=output_token_parameter,
|
|
1237
|
+
)
|
|
1238
|
+
async with owning_async_iterator(iterator, owner="studio.anthropic"):
|
|
1239
|
+
async for chunk in iterator:
|
|
1240
|
+
yield chunk
|
|
1241
|
+
return
|
|
1242
|
+
await self.network_guard.check(model.endpoint_url, network_policy)
|
|
1243
|
+
credential = self.credential_resolver.resolve(model.credential_ref)
|
|
1244
|
+
profile = compatibility_profile or provider_compat.compat_profile(
|
|
1245
|
+
base_url=model.endpoint_url, model=model.model
|
|
1246
|
+
)
|
|
1247
|
+
payload: dict[str, Any] = {
|
|
1248
|
+
"model": model.model,
|
|
1249
|
+
"messages": messages,
|
|
1250
|
+
"stream": True,
|
|
1251
|
+
# OpenAI-compatible chat streams do not have to include usage in
|
|
1252
|
+
# ordinary delta chunks. Request the terminal usage chunk when
|
|
1253
|
+
# the endpoint profile supports it; ``apply_chat_compat`` removes
|
|
1254
|
+
# this option for strict providers that reject the extension.
|
|
1255
|
+
"stream_options": {"include_usage": True},
|
|
1256
|
+
}
|
|
1257
|
+
if model.parameters.temperature is not None:
|
|
1258
|
+
payload["temperature"] = model.parameters.temperature
|
|
1259
|
+
if model.parameters.max_tokens is not None:
|
|
1260
|
+
payload["max_tokens"] = model.parameters.max_tokens
|
|
1261
|
+
if max_output_tokens is not None:
|
|
1262
|
+
payload["max_tokens"] = max_output_tokens
|
|
1263
|
+
if model.parameters.top_p is not None:
|
|
1264
|
+
payload["top_p"] = model.parameters.top_p
|
|
1265
|
+
if tools:
|
|
1266
|
+
payload["tools"] = tools
|
|
1267
|
+
payload["tool_choice"] = "auto"
|
|
1268
|
+
payload["parallel_tool_calls"] = True
|
|
1269
|
+
payload = provider_compat.apply_chat_compat(payload, profile, model=model.model)
|
|
1270
|
+
self._apply_tool_choice(payload, "chat", tool_choice)
|
|
1271
|
+
self._apply_parallel_tool_policy(payload, "chat", parallel_tool_calls)
|
|
1272
|
+
# Fixed allocations are owned by the caller, including empty-stream
|
|
1273
|
+
# fallback. Only the legacy adaptive path may apply recommendations.
|
|
1274
|
+
if max_output_tokens is not None and output_budget_mode == "adaptive":
|
|
1275
|
+
effective_budget = provider_compat.required_output_budget(
|
|
1276
|
+
profile, payload.get("max_tokens")
|
|
1277
|
+
)
|
|
1278
|
+
if effective_budget is not None:
|
|
1279
|
+
payload["max_tokens"] = effective_budget
|
|
1280
|
+
effective_effort = _effective_reasoning_effort(model, tools, reasoning_effort, profile)
|
|
1281
|
+
self._apply_reasoning_effort(payload, "chat", effective_effort)
|
|
1282
|
+
self._apply_thinking_policy(payload, "chat", profile, tools, effective_effort,
|
|
1283
|
+
thinking_mode=thinking_mode)
|
|
1284
|
+
payload = provider_compat.apply_chat_compat(payload, profile, model=model.model)
|
|
1285
|
+
self._apply_output_token_parameter(payload, "chat", output_token_parameter)
|
|
1286
|
+
headers = {
|
|
1287
|
+
"Authorization": f"Bearer {credential}",
|
|
1288
|
+
"Content-Type": "application/json",
|
|
1289
|
+
"Accept": "text/event-stream",
|
|
1290
|
+
}
|
|
1291
|
+
timeout = httpx.Timeout(timeout_seconds, connect=min(10, timeout_seconds))
|
|
1292
|
+
client = self._http_client()
|
|
1293
|
+
request_started = time.monotonic()
|
|
1294
|
+
|
|
1295
|
+
async def recover_empty_response() -> ModelResponse:
|
|
1296
|
+
"""Ask once for a usable answer after a reasoning-only stream.
|
|
1297
|
+
|
|
1298
|
+
Some OpenAI-compatible reasoning models occasionally finish a
|
|
1299
|
+
stream after emitting only ``reasoning_content``. Replaying the
|
|
1300
|
+
exact same request through the outer ten-attempt provider policy
|
|
1301
|
+
is both slow and usually deterministic. Give the provider one
|
|
1302
|
+
explicit, non-streaming repair turn instead; if that is empty as
|
|
1303
|
+
well, preserve ``MODEL_EMPTY_RESPONSE`` so the Harness can stop
|
|
1304
|
+
immediately rather than presenting it as transient congestion.
|
|
1305
|
+
"""
|
|
1306
|
+
|
|
1307
|
+
return await self.complete(
|
|
1308
|
+
model,
|
|
1309
|
+
messages=[
|
|
1310
|
+
*messages,
|
|
1311
|
+
{
|
|
1312
|
+
"role": "system",
|
|
1313
|
+
"content": (
|
|
1314
|
+
"The previous response contained no usable final answer or tool "
|
|
1315
|
+
"call. Return either a concrete final answer or one valid native "
|
|
1316
|
+
"tool call now. Do not return reasoning alone."
|
|
1317
|
+
),
|
|
1318
|
+
},
|
|
1319
|
+
],
|
|
1320
|
+
network_policy=network_policy,
|
|
1321
|
+
timeout_seconds=timeout_seconds,
|
|
1322
|
+
max_attempts=1,
|
|
1323
|
+
backoff_seconds=0,
|
|
1324
|
+
tools=tools,
|
|
1325
|
+
retry_on_length=True,
|
|
1326
|
+
max_output_tokens=max_output_tokens,
|
|
1327
|
+
output_budget_mode=output_budget_mode,
|
|
1328
|
+
reasoning_effort=reasoning_effort,
|
|
1329
|
+
thinking_mode=thinking_mode,
|
|
1330
|
+
compatibility_profile=profile,
|
|
1331
|
+
**({"tool_choice": tool_choice} if tool_choice is not None else {}),
|
|
1332
|
+
**({"parallel_tool_calls": parallel_tool_calls}
|
|
1333
|
+
if parallel_tool_calls is not None else {}),
|
|
1334
|
+
**({"output_token_parameter": output_token_parameter}
|
|
1335
|
+
if output_token_parameter is not None else {}),
|
|
1336
|
+
)
|
|
1337
|
+
|
|
1338
|
+
try:
|
|
1339
|
+
response_context = client.stream(
|
|
1340
|
+
"POST", model.endpoint_url, headers=headers, json=payload, timeout=timeout
|
|
1341
|
+
)
|
|
1342
|
+
async with owning_async_context(response_context, owner="studio.chat_http") as response:
|
|
1343
|
+
if response.is_redirect or response.status_code >= 400:
|
|
1344
|
+
raise await _http_model_error(response, wire_api="chat")
|
|
1345
|
+
provider_request_id = _provider_request_id(response.headers)
|
|
1346
|
+
tool_stream = ChatToolCalls()
|
|
1347
|
+
first_chunk_logged = False
|
|
1348
|
+
raw_text = ""
|
|
1349
|
+
content_stream = ChatContent()
|
|
1350
|
+
emitted_text = ""
|
|
1351
|
+
pending_visible_text = ""
|
|
1352
|
+
textual_tool_detected = False
|
|
1353
|
+
latest_usage: Usage | None = None
|
|
1354
|
+
pending_done_finish_reason = ""
|
|
1355
|
+
refused = False
|
|
1356
|
+
wire_identity = ModelWireIdentity()
|
|
1357
|
+
last_repetition_check_length = 0
|
|
1358
|
+
async with _model_frames(
|
|
1359
|
+
response, idle_timeout_seconds=timeout_seconds,
|
|
1360
|
+
known_policy_abort=lambda: refused,
|
|
1361
|
+
) as frames:
|
|
1362
|
+
async for frame in frames:
|
|
1363
|
+
raw = frame.data.strip()
|
|
1364
|
+
if raw == "[DONE]":
|
|
1365
|
+
break
|
|
1366
|
+
if not raw:
|
|
1367
|
+
continue
|
|
1368
|
+
chunk = decode_model_frame(raw)
|
|
1369
|
+
wire_identity.accept(chunk, wire_api="chat")
|
|
1370
|
+
failure = provider_error(
|
|
1371
|
+
chunk, wire_api="chat", headers=response.headers,
|
|
1372
|
+
provider_request_id=provider_request_id,
|
|
1373
|
+
usage=provider_usage(chunk.get("usage"), wire_api="chat"),
|
|
1374
|
+
)
|
|
1375
|
+
if failure is not None:
|
|
1376
|
+
raise failure
|
|
1377
|
+
choices = chat_choices(chunk)
|
|
1378
|
+
finish_reason = next(
|
|
1379
|
+
(
|
|
1380
|
+
choice["finish_reason"]
|
|
1381
|
+
for choice in choices
|
|
1382
|
+
if choice.get("finish_reason")
|
|
1383
|
+
),
|
|
1384
|
+
"",
|
|
1385
|
+
)
|
|
1386
|
+
if (
|
|
1387
|
+
finish_reason
|
|
1388
|
+
and pending_done_finish_reason
|
|
1389
|
+
and finish_reason != pending_done_finish_reason
|
|
1390
|
+
):
|
|
1391
|
+
raise invalid_model_frame()
|
|
1392
|
+
usage_value = provider_usage(chunk.get("usage"), wire_api="chat")
|
|
1393
|
+
if usage_value is not None:
|
|
1394
|
+
latest_usage = usage_value
|
|
1395
|
+
if usage_value is not None and not choices:
|
|
1396
|
+
# A usage-only frame can precede or follow the finish.
|
|
1397
|
+
# Report it now, so later absent/invalid data cannot
|
|
1398
|
+
# erase known counters. Do not synthesize a terminal.
|
|
1399
|
+
yield ModelStreamChunk(
|
|
1400
|
+
usage=usage_value, provider_request_id=provider_request_id,
|
|
1401
|
+
)
|
|
1402
|
+
if not choices and chunk.get("choices"):
|
|
1403
|
+
# Explicit alternative-only packets have no selected
|
|
1404
|
+
# choice. Their root snapshots cannot be attributed to
|
|
1405
|
+
# choice zero; keep only this attempt's known usage.
|
|
1406
|
+
continue
|
|
1407
|
+
root_message = chunk.get("message") or {}
|
|
1408
|
+
try:
|
|
1409
|
+
# Deltas precede same-frame complete snapshots. A
|
|
1410
|
+
# snapshot may already include that last fragment.
|
|
1411
|
+
for choice in choices:
|
|
1412
|
+
tool_stream.accept(choice.get("delta"), snapshot=False)
|
|
1413
|
+
tool_stream.accept(choice.get("message"), snapshot=True)
|
|
1414
|
+
tool_stream.accept(choice, snapshot=True)
|
|
1415
|
+
tool_stream.accept(root_message, snapshot=True)
|
|
1416
|
+
tool_stream.accept(chunk, snapshot=True)
|
|
1417
|
+
except StudioError as error:
|
|
1418
|
+
raise ModelResponseError(error, latest_usage) from None
|
|
1419
|
+
refused = refused or wire_refusal(
|
|
1420
|
+
root_message.get("refusal"), wire_api="chat",
|
|
1421
|
+
)
|
|
1422
|
+
content_choices = choices or ([{}] if any(
|
|
1423
|
+
root_message.get(key) is not None
|
|
1424
|
+
for key in ("content", "reasoning_content", "reasoning")
|
|
1425
|
+
) else [])
|
|
1426
|
+
for choice in content_choices:
|
|
1427
|
+
delta = choice.get("delta") or {}
|
|
1428
|
+
refused = wire_refusal(delta.get("refusal"), wire_api="chat") or refused
|
|
1429
|
+
try:
|
|
1430
|
+
text, reasoning = content_stream.accept(
|
|
1431
|
+
delta, choice.get("message") or {}, root_message,
|
|
1432
|
+
)
|
|
1433
|
+
except StudioError as error:
|
|
1434
|
+
raise ModelResponseError(error, latest_usage) from None
|
|
1435
|
+
raw_text += text
|
|
1436
|
+
pending_visible_text += text
|
|
1437
|
+
message = choice.get("message") or {}
|
|
1438
|
+
refused = wire_refusal(message.get("refusal"), wire_api="chat") \
|
|
1439
|
+
or refused
|
|
1440
|
+
visible_text = ""
|
|
1441
|
+
if text and not textual_tool_detected:
|
|
1442
|
+
visible_text, pending_visible_text, marker_found = (
|
|
1443
|
+
_partition_tool_stream_text(pending_visible_text)
|
|
1444
|
+
)
|
|
1445
|
+
textual_tool_detected = marker_found
|
|
1446
|
+
emitted_text += visible_text
|
|
1447
|
+
visible_length = len(emitted_text) + len(pending_visible_text)
|
|
1448
|
+
if (
|
|
1449
|
+
not textual_tool_detected
|
|
1450
|
+
and visible_length - last_repetition_check_length >= 64
|
|
1451
|
+
):
|
|
1452
|
+
last_repetition_check_length = visible_length
|
|
1453
|
+
_raise_for_repetitive_output(
|
|
1454
|
+
emitted_text + pending_visible_text
|
|
1455
|
+
)
|
|
1456
|
+
if visible_text or reasoning or usage_value is not None:
|
|
1457
|
+
if not first_chunk_logged and (text or reasoning):
|
|
1458
|
+
first_chunk_logged = True
|
|
1459
|
+
_LOGGER.info(
|
|
1460
|
+
"model stream first delta: model=%s ttfb_ms=%d",
|
|
1461
|
+
model.model,
|
|
1462
|
+
int((time.monotonic() - request_started) * 1000),
|
|
1463
|
+
)
|
|
1464
|
+
yield ModelStreamChunk(
|
|
1465
|
+
text=visible_text,
|
|
1466
|
+
reasoning=reasoning,
|
|
1467
|
+
usage=usage_value,
|
|
1468
|
+
provider_request_id=provider_request_id,
|
|
1469
|
+
)
|
|
1470
|
+
if finish_reason:
|
|
1471
|
+
pending_done_finish_reason = finish_reason
|
|
1472
|
+
content_stream.finished = True
|
|
1473
|
+
tool_stream.finished = True
|
|
1474
|
+
# A finish notification does not seal the HTTP response. Keep
|
|
1475
|
+
# consuming its usage and snapshots until [DONE] or natural
|
|
1476
|
+
# EOF, then publish exactly one final tool batch. A read error
|
|
1477
|
+
# before that boundary cannot authorize tool effects.
|
|
1478
|
+
if refused:
|
|
1479
|
+
yield ModelStreamChunk(
|
|
1480
|
+
usage=latest_usage, done=True, finish_reason="content_filter",
|
|
1481
|
+
provider_request_id=provider_request_id,
|
|
1482
|
+
)
|
|
1483
|
+
return
|
|
1484
|
+
empty_output_limit_retry = (
|
|
1485
|
+
retry_empty_stream and not tool_stream.calls and not raw_text.strip()
|
|
1486
|
+
and pending_done_finish_reason in {
|
|
1487
|
+
"length", "incomplete", "max_tokens", "max_output_tokens", "output_limit",
|
|
1488
|
+
}
|
|
1489
|
+
)
|
|
1490
|
+
if (pending_done_finish_reason in _INCOMPLETE_CALL_NON_SUCCESS_FINISH_REASONS
|
|
1491
|
+
and not empty_output_limit_retry):
|
|
1492
|
+
# An explicit non-success termination cannot authorize any
|
|
1493
|
+
# partially assembled tool. Preserve its reason; do not
|
|
1494
|
+
# retry. Explicit empty-stream compatibility repair is
|
|
1495
|
+
# limited to output caps with no tool envelope. An unfilled
|
|
1496
|
+
# call under a success/missing reason
|
|
1497
|
+
# still falls through to tool_stream.finish() and surfaces
|
|
1498
|
+
# as a provider_retryable_error.
|
|
1499
|
+
yield ModelStreamChunk(
|
|
1500
|
+
tool_calls=(), usage=latest_usage, done=True,
|
|
1501
|
+
finish_reason=pending_done_finish_reason,
|
|
1502
|
+
provider_request_id=provider_request_id,
|
|
1503
|
+
)
|
|
1504
|
+
return
|
|
1505
|
+
try:
|
|
1506
|
+
sealed_tools = tool_stream.finish()
|
|
1507
|
+
except StudioError as error:
|
|
1508
|
+
raise ModelResponseError(error, latest_usage) from None
|
|
1509
|
+
calls = tuple(ToolCall(id=value.call_id, name=value.name, arguments=value.arguments)
|
|
1510
|
+
for value in sealed_tools)
|
|
1511
|
+
final_text = raw_text
|
|
1512
|
+
if not calls and (
|
|
1513
|
+
textual_tool_detected
|
|
1514
|
+
or "DSML" in raw_text.replace("|", "|").upper()
|
|
1515
|
+
or "<tool_call" in raw_text.lower()
|
|
1516
|
+
):
|
|
1517
|
+
final_text, recovered_calls, _ = _recover_textual_tool_calls(raw_text, tools)
|
|
1518
|
+
calls = tuple(recovered_calls)
|
|
1519
|
+
calls = _normalize_tool_call_arguments(calls, tools)
|
|
1520
|
+
if not final_text.strip() and not calls and retry_empty_stream:
|
|
1521
|
+
repair = await recover_empty_response()
|
|
1522
|
+
final_text = repair.content
|
|
1523
|
+
calls = tuple(repair.tool_calls)
|
|
1524
|
+
latest_usage = repair.usage
|
|
1525
|
+
if not calls and not textual_tool_detected:
|
|
1526
|
+
_raise_for_repetitive_output(final_text)
|
|
1527
|
+
remaining_text = final_text
|
|
1528
|
+
if emitted_text and final_text.startswith(emitted_text):
|
|
1529
|
+
remaining_text = final_text[len(emitted_text) :]
|
|
1530
|
+
if remaining_text:
|
|
1531
|
+
yield ModelStreamChunk(
|
|
1532
|
+
text=remaining_text,
|
|
1533
|
+
provider_request_id=provider_request_id,
|
|
1534
|
+
)
|
|
1535
|
+
yield ModelStreamChunk(
|
|
1536
|
+
tool_calls=calls,
|
|
1537
|
+
usage=latest_usage,
|
|
1538
|
+
done=True,
|
|
1539
|
+
finish_reason=pending_done_finish_reason,
|
|
1540
|
+
provider_request_id=provider_request_id,
|
|
1541
|
+
)
|
|
1542
|
+
except asyncio.CancelledError:
|
|
1543
|
+
raise
|
|
1544
|
+
|
|
1545
|
+
async def _stream_responses(
|
|
1546
|
+
self,
|
|
1547
|
+
model: ResolvedModel,
|
|
1548
|
+
*,
|
|
1549
|
+
messages: list[dict[str, Any]],
|
|
1550
|
+
network_policy: NetworkPolicy,
|
|
1551
|
+
timeout_seconds: float,
|
|
1552
|
+
tools: list[dict[str, Any]] | None = None,
|
|
1553
|
+
max_output_tokens: int | None = None,
|
|
1554
|
+
reasoning_effort: str | None = None,
|
|
1555
|
+
compatibility_profile: provider_compat.CompatProfile | None = None,
|
|
1556
|
+
thinking_mode: Literal["disabled"] | None = None,
|
|
1557
|
+
tool_choice: str | dict[str, Any] | None = None,
|
|
1558
|
+
parallel_tool_calls: bool | None = None,
|
|
1559
|
+
output_token_parameter: Literal["max_tokens", "max_completion_tokens"] | None = None,
|
|
1560
|
+
) -> AsyncIterator[ModelStreamChunk]:
|
|
1561
|
+
"""Adapt the native Responses SSE event stream to Harness chunks."""
|
|
1562
|
+
|
|
1563
|
+
await self.network_guard.check(model.endpoint_url, network_policy)
|
|
1564
|
+
credential = self.credential_resolver.resolve(model.credential_ref)
|
|
1565
|
+
payload = self._responses_payload(model, messages, tools)
|
|
1566
|
+
self._apply_tool_choice(payload, "responses", tool_choice)
|
|
1567
|
+
self._apply_parallel_tool_policy(payload, "responses", parallel_tool_calls)
|
|
1568
|
+
self._apply_output_token_parameter(payload, "responses", output_token_parameter)
|
|
1569
|
+
payload["stream"] = True
|
|
1570
|
+
if max_output_tokens is not None:
|
|
1571
|
+
payload["max_output_tokens"] = max_output_tokens
|
|
1572
|
+
if reasoning_effort:
|
|
1573
|
+
profile = compatibility_profile or provider_compat.compat_profile(
|
|
1574
|
+
base_url=model.endpoint_url, model=model.model,
|
|
1575
|
+
)
|
|
1576
|
+
self._apply_reasoning_effort(
|
|
1577
|
+
payload, "responses", _effective_reasoning_effort(
|
|
1578
|
+
model, None, reasoning_effort, profile,
|
|
1579
|
+
),
|
|
1580
|
+
)
|
|
1581
|
+
headers = {
|
|
1582
|
+
"Authorization": f"Bearer {credential}",
|
|
1583
|
+
"Content-Type": "application/json",
|
|
1584
|
+
"Accept": "text/event-stream",
|
|
1585
|
+
}
|
|
1586
|
+
timeout = httpx.Timeout(timeout_seconds, connect=min(10, timeout_seconds))
|
|
1587
|
+
client = self._http_client()
|
|
1588
|
+
request_started = time.monotonic()
|
|
1589
|
+
first_delta_logged = False
|
|
1590
|
+
tool_items = ResponsesToolCalls()
|
|
1591
|
+
content_items = ResponsesContent(tool_items.items)
|
|
1592
|
+
from ksadk.harness.public_summary import PublicSummaryBuffer
|
|
1593
|
+
|
|
1594
|
+
summary_buffer = PublicSummaryBuffer()
|
|
1595
|
+
raw_text = ""
|
|
1596
|
+
raw_reasoning = ""
|
|
1597
|
+
refused = False
|
|
1598
|
+
wire_identity = ModelWireIdentity()
|
|
1599
|
+
response_context = client.stream(
|
|
1600
|
+
"POST", model.endpoint_url, headers=headers, json=payload, timeout=timeout
|
|
1601
|
+
)
|
|
1602
|
+
async with owning_async_context(
|
|
1603
|
+
response_context, owner="studio.responses_http"
|
|
1604
|
+
) as response:
|
|
1605
|
+
if response.is_redirect or response.status_code >= 400:
|
|
1606
|
+
raise await _http_model_error(response, wire_api="responses")
|
|
1607
|
+
provider_request_id = _provider_request_id(response.headers)
|
|
1608
|
+
async with _model_frames(
|
|
1609
|
+
response, idle_timeout_seconds=timeout_seconds,
|
|
1610
|
+
known_policy_abort=lambda: refused,
|
|
1611
|
+
) as frames:
|
|
1612
|
+
async for frame in frames:
|
|
1613
|
+
raw = frame.data.strip()
|
|
1614
|
+
if not raw or raw == "[DONE]":
|
|
1615
|
+
continue
|
|
1616
|
+
event = decode_model_frame(raw)
|
|
1617
|
+
wire_identity.accept(event, wire_api="responses")
|
|
1618
|
+
if event.get("type") in {"error", "response.failed"}:
|
|
1619
|
+
failed_body = event.get("response")
|
|
1620
|
+
if failed_body is not None and not isinstance(failed_body, dict):
|
|
1621
|
+
raise invalid_model_frame()
|
|
1622
|
+
if event["type"] == "response.failed":
|
|
1623
|
+
failed_body = dict(failed_body or {})
|
|
1624
|
+
if failed_body.get("status") not in (None, "failed"):
|
|
1625
|
+
raise invalid_model_frame()
|
|
1626
|
+
failed_body["status"] = "failed"
|
|
1627
|
+
else:
|
|
1628
|
+
failed_body = {"error": event.get("error", event),
|
|
1629
|
+
"usage": event.get("usage")}
|
|
1630
|
+
failure = provider_error(
|
|
1631
|
+
failed_body, wire_api="responses", headers=response.headers,
|
|
1632
|
+
provider_request_id=provider_request_id,
|
|
1633
|
+
usage=provider_usage(failed_body.get("usage"), wire_api="responses"),
|
|
1634
|
+
)
|
|
1635
|
+
assert failure is not None
|
|
1636
|
+
raise failure
|
|
1637
|
+
completed_tool = tool_items.accept(event)
|
|
1638
|
+
text, reasoning = content_items.accept(event)
|
|
1639
|
+
event_type = str(event.get("type") or "")
|
|
1640
|
+
public_summary = summary_buffer.drain(content_items.public_summary(), final=(
|
|
1641
|
+
event_type in {"response.completed", "response.incomplete"}
|
|
1642
|
+
))
|
|
1643
|
+
if public_summary and model.parameters.public_thinking_summary:
|
|
1644
|
+
yield ModelStreamChunk(public_summary=public_summary,
|
|
1645
|
+
provider_request_id=provider_request_id)
|
|
1646
|
+
refused = refused or content_items.refused
|
|
1647
|
+
usage_value: Usage | None = None
|
|
1648
|
+
if event_type in {"response.completed", "response.incomplete"}:
|
|
1649
|
+
response_payload = event.get("response") or {}
|
|
1650
|
+
usage_value = provider_usage(
|
|
1651
|
+
response_payload.get("usage"), wire_api="responses",
|
|
1652
|
+
)
|
|
1653
|
+
elif event_type in {
|
|
1654
|
+
"error", "response.failed", "response.cancelled", "response.canceled",
|
|
1655
|
+
}:
|
|
1656
|
+
raise StudioError(
|
|
1657
|
+
"MODEL_REQUEST_FAILED",
|
|
1658
|
+
"模型 Responses 流式响应未完成",
|
|
1659
|
+
status_code=502,
|
|
1660
|
+
details={"eventType": event_type},
|
|
1661
|
+
)
|
|
1662
|
+
if text or reasoning:
|
|
1663
|
+
raw_text += text
|
|
1664
|
+
raw_reasoning += reasoning
|
|
1665
|
+
if not first_delta_logged:
|
|
1666
|
+
first_delta_logged = True
|
|
1667
|
+
_LOGGER.info(
|
|
1668
|
+
"model Responses stream first delta: model=%s ttfb_ms=%d",
|
|
1669
|
+
model.model,
|
|
1670
|
+
int((time.monotonic() - request_started) * 1000),
|
|
1671
|
+
)
|
|
1672
|
+
yield ModelStreamChunk(
|
|
1673
|
+
text=text,
|
|
1674
|
+
reasoning=reasoning,
|
|
1675
|
+
provider_request_id=provider_request_id,
|
|
1676
|
+
)
|
|
1677
|
+
if completed_tool is not None and not refused:
|
|
1678
|
+
yield ModelStreamChunk(
|
|
1679
|
+
tool_calls=(ToolCall(
|
|
1680
|
+
id=completed_tool.call_id, name=completed_tool.name,
|
|
1681
|
+
arguments=completed_tool.arguments or "{}",
|
|
1682
|
+
),),
|
|
1683
|
+
provider_request_id=provider_request_id,
|
|
1684
|
+
)
|
|
1685
|
+
if event_type in {"response.completed", "response.incomplete"}:
|
|
1686
|
+
try:
|
|
1687
|
+
snapshot = reconcile_responses_snapshot(
|
|
1688
|
+
response_payload,
|
|
1689
|
+
status="completed" if event_type == "response.completed"
|
|
1690
|
+
else "incomplete",
|
|
1691
|
+
tools=tool_items, text=raw_text, reasoning=raw_reasoning,
|
|
1692
|
+
content=content_items,
|
|
1693
|
+
)
|
|
1694
|
+
except StudioError as error:
|
|
1695
|
+
raise ModelResponseError(error, usage_value) from None
|
|
1696
|
+
if snapshot is not None:
|
|
1697
|
+
refused = refused or snapshot.projection.finish == "content_filter"
|
|
1698
|
+
if not refused:
|
|
1699
|
+
if snapshot.text_tail or snapshot.reasoning_tail:
|
|
1700
|
+
yield ModelStreamChunk(
|
|
1701
|
+
text=snapshot.text_tail, reasoning=snapshot.reasoning_tail,
|
|
1702
|
+
provider_request_id=provider_request_id,
|
|
1703
|
+
)
|
|
1704
|
+
for tool in snapshot.completed_calls:
|
|
1705
|
+
yield ModelStreamChunk(tool_calls=(ToolCall(
|
|
1706
|
+
id=tool.call_id, name=tool.name,
|
|
1707
|
+
arguments=tool.arguments or "{}",
|
|
1708
|
+
),), provider_request_id=provider_request_id)
|
|
1709
|
+
if refused:
|
|
1710
|
+
yield ModelStreamChunk(
|
|
1711
|
+
usage=usage_value, done=True, finish_reason="content_filter",
|
|
1712
|
+
provider_request_id=provider_request_id,
|
|
1713
|
+
)
|
|
1714
|
+
return
|
|
1715
|
+
if event_type == "response.completed":
|
|
1716
|
+
try:
|
|
1717
|
+
tool_items.finish()
|
|
1718
|
+
content_items.finish()
|
|
1719
|
+
except StudioError as error:
|
|
1720
|
+
raise ModelResponseError(error, usage_value) from None
|
|
1721
|
+
finish_reason = "tool_calls" if tool_items.calls else "stop"
|
|
1722
|
+
else:
|
|
1723
|
+
details = response_payload.get("incomplete_details")
|
|
1724
|
+
if details is not None and not isinstance(details, dict):
|
|
1725
|
+
raise invalid_model_frame()
|
|
1726
|
+
reason = (details or {}).get("reason")
|
|
1727
|
+
if reason is not None and type(reason) is not str:
|
|
1728
|
+
raise invalid_model_frame()
|
|
1729
|
+
if reason == "max_output_tokens":
|
|
1730
|
+
try:
|
|
1731
|
+
content_items.finish(require_closed=False)
|
|
1732
|
+
except StudioError as error:
|
|
1733
|
+
raise ModelResponseError(error, usage_value) from None
|
|
1734
|
+
# An unclosed input must not grant bounded-text commit
|
|
1735
|
+
# authority even when output exhaustion is confirmed.
|
|
1736
|
+
open_input = any(not tool.closed for tool in tool_items.calls.values())
|
|
1737
|
+
finish_reason = (
|
|
1738
|
+
"content_filter" if reason == "content_filter"
|
|
1739
|
+
else "length" if reason == "max_output_tokens" and not open_input
|
|
1740
|
+
else "incomplete"
|
|
1741
|
+
)
|
|
1742
|
+
if snapshot is not None and snapshot.projection.calls:
|
|
1743
|
+
finish_reason = snapshot.projection.finish
|
|
1744
|
+
yield ModelStreamChunk(
|
|
1745
|
+
usage=usage_value, done=True, finish_reason=finish_reason,
|
|
1746
|
+
provider_request_id=provider_request_id,
|
|
1747
|
+
)
|
|
1748
|
+
return
|
|
1749
|
+
|
|
1750
|
+
if refused:
|
|
1751
|
+
yield ModelStreamChunk(
|
|
1752
|
+
done=True, finish_reason="content_filter",
|
|
1753
|
+
provider_request_id=provider_request_id,
|
|
1754
|
+
)
|
|
1755
|
+
|
|
1756
|
+
async def _stream_anthropic(
|
|
1757
|
+
self,
|
|
1758
|
+
model: ResolvedModel,
|
|
1759
|
+
*,
|
|
1760
|
+
messages: list[dict[str, Any]],
|
|
1761
|
+
network_policy: NetworkPolicy,
|
|
1762
|
+
timeout_seconds: float,
|
|
1763
|
+
tools: list[dict[str, Any]] | None = None,
|
|
1764
|
+
max_output_tokens: int | None = None,
|
|
1765
|
+
reasoning_effort: str | None = None,
|
|
1766
|
+
compatibility_profile: provider_compat.CompatProfile | None = None,
|
|
1767
|
+
thinking_mode: Literal["disabled"] | None = None,
|
|
1768
|
+
tool_choice: str | dict[str, Any] | None = None,
|
|
1769
|
+
parallel_tool_calls: bool | None = None,
|
|
1770
|
+
output_token_parameter: Literal["max_tokens", "max_completion_tokens"] | None = None,
|
|
1771
|
+
) -> AsyncIterator[ModelStreamChunk]:
|
|
1772
|
+
"""Adapt the native Anthropic Messages SSE event stream to Harness chunks."""
|
|
1773
|
+
|
|
1774
|
+
await self.network_guard.check(model.endpoint_url, network_policy)
|
|
1775
|
+
credential = self.credential_resolver.resolve(model.credential_ref)
|
|
1776
|
+
profile = compatibility_profile or provider_compat.compat_profile(
|
|
1777
|
+
base_url=model.endpoint_url, model=model.model,
|
|
1778
|
+
)
|
|
1779
|
+
payload = self._anthropic_payload(model, messages, tools, compatibility_profile=profile)
|
|
1780
|
+
self._apply_tool_choice(payload, "anthropic", tool_choice)
|
|
1781
|
+
self._apply_parallel_tool_policy(payload, "anthropic", parallel_tool_calls)
|
|
1782
|
+
self._apply_output_token_parameter(payload, "anthropic", output_token_parameter)
|
|
1783
|
+
payload["stream"] = True
|
|
1784
|
+
if max_output_tokens is not None:
|
|
1785
|
+
payload["max_tokens"] = max_output_tokens
|
|
1786
|
+
effective_effort = _effective_reasoning_effort(model, tools, reasoning_effort, profile)
|
|
1787
|
+
self._apply_reasoning_effort(
|
|
1788
|
+
payload, "anthropic", effective_effort,
|
|
1789
|
+
)
|
|
1790
|
+
self._apply_thinking_policy(payload, "anthropic", profile, tools, effective_effort,
|
|
1791
|
+
thinking_mode=thinking_mode)
|
|
1792
|
+
headers = self._anthropic_headers(credential, payload)
|
|
1793
|
+
headers["Accept"] = "text/event-stream"
|
|
1794
|
+
timeout = httpx.Timeout(timeout_seconds, connect=min(10, timeout_seconds))
|
|
1795
|
+
client = self._http_client()
|
|
1796
|
+
request_started = time.monotonic()
|
|
1797
|
+
first_delta_logged = False
|
|
1798
|
+
# content_block index → tool_use block 累积(id/name 来自 block_start,参数来自分片)。
|
|
1799
|
+
tool_blocks = AnthropicToolBlocks()
|
|
1800
|
+
content_stream = AnthropicContent(tool_blocks)
|
|
1801
|
+
raw_text = ""
|
|
1802
|
+
stop_reason = ""
|
|
1803
|
+
refused = False
|
|
1804
|
+
usage_accumulator = AnthropicUsageAccumulator()
|
|
1805
|
+
wire_identity = ModelWireIdentity()
|
|
1806
|
+
response_context = client.stream(
|
|
1807
|
+
"POST", model.endpoint_url, headers=headers, json=payload, timeout=timeout
|
|
1808
|
+
)
|
|
1809
|
+
async with owning_async_context(
|
|
1810
|
+
response_context, owner="studio.anthropic_http"
|
|
1811
|
+
) as response:
|
|
1812
|
+
if response.is_redirect or response.status_code >= 400:
|
|
1813
|
+
raise await _http_model_error(response, wire_api="anthropic")
|
|
1814
|
+
async with _model_frames(
|
|
1815
|
+
response, idle_timeout_seconds=timeout_seconds,
|
|
1816
|
+
known_policy_abort=lambda: refused or stop_reason == "refusal",
|
|
1817
|
+
) as frames:
|
|
1818
|
+
async for frame in frames:
|
|
1819
|
+
raw = frame.data.strip()
|
|
1820
|
+
if not raw or raw == "[DONE]":
|
|
1821
|
+
continue
|
|
1822
|
+
event = decode_model_frame(raw)
|
|
1823
|
+
wire_identity.accept(event, wire_api="anthropic")
|
|
1824
|
+
if event.get("type") == "error":
|
|
1825
|
+
failure = provider_error(
|
|
1826
|
+
event, wire_api="anthropic", headers=response.headers,
|
|
1827
|
+
provider_request_id=_provider_request_id(response.headers),
|
|
1828
|
+
usage=provider_usage(event.get("usage"), wire_api="anthropic"),
|
|
1829
|
+
)
|
|
1830
|
+
if failure is None:
|
|
1831
|
+
raise invalid_model_frame()
|
|
1832
|
+
raise failure
|
|
1833
|
+
tool_blocks.accept(event)
|
|
1834
|
+
text, reasoning = content_stream.accept(event)
|
|
1835
|
+
event_type = str(event.get("type") or "")
|
|
1836
|
+
usage_update: Usage | None = None
|
|
1837
|
+
if event_type == "message_start":
|
|
1838
|
+
message_usage = (event.get("message") or {}).get("usage")
|
|
1839
|
+
usage_update = usage_accumulator.update(message_usage)
|
|
1840
|
+
elif event_type == "message_delta":
|
|
1841
|
+
delta = event.get("delta") or {}
|
|
1842
|
+
incoming_stop = delta.get("stop_reason")
|
|
1843
|
+
if incoming_stop:
|
|
1844
|
+
if stop_reason and incoming_stop != stop_reason:
|
|
1845
|
+
raise invalid_model_frame()
|
|
1846
|
+
stop_reason = incoming_stop
|
|
1847
|
+
content_stream.finished = True
|
|
1848
|
+
usage_update = usage_accumulator.update(event.get("usage"))
|
|
1849
|
+
try:
|
|
1850
|
+
refused = wire_refusal(
|
|
1851
|
+
delta.get("stop_details"), wire_api="anthropic",
|
|
1852
|
+
) or refused
|
|
1853
|
+
except StudioError as error:
|
|
1854
|
+
raise ModelResponseError(error, usage_accumulator.report()) from None
|
|
1855
|
+
elif event_type == "error":
|
|
1856
|
+
raise StudioError(
|
|
1857
|
+
"MODEL_REQUEST_FAILED",
|
|
1858
|
+
"模型 Anthropic 流式响应未完成",
|
|
1859
|
+
status_code=502,
|
|
1860
|
+
details={
|
|
1861
|
+
"eventType": str(
|
|
1862
|
+
(event.get("error") or {}).get("type") or event_type
|
|
1863
|
+
)
|
|
1864
|
+
},
|
|
1865
|
+
)
|
|
1866
|
+
if event_type in {"message_start", "message_delta"}:
|
|
1867
|
+
fields = event["message"] if event_type == "message_start" else event
|
|
1868
|
+
try:
|
|
1869
|
+
observed = input_observation(fields, payload, event_type)
|
|
1870
|
+
except (ValueError, TypeError):
|
|
1871
|
+
raise ModelResponseError(
|
|
1872
|
+
invalid_model_frame(), usage_accumulator.report(),
|
|
1873
|
+
) from None
|
|
1874
|
+
if observed is not None:
|
|
1875
|
+
# Final fallback metadata replaces the start array, including [].
|
|
1876
|
+
yield ModelStreamChunk(provider_input=observed,
|
|
1877
|
+
provider_request_id=_provider_request_id(
|
|
1878
|
+
response.headers,
|
|
1879
|
+
))
|
|
1880
|
+
raw_text += text
|
|
1881
|
+
if text or reasoning or usage_update is not None:
|
|
1882
|
+
if not first_delta_logged and (text or reasoning):
|
|
1883
|
+
first_delta_logged = True
|
|
1884
|
+
_LOGGER.info(
|
|
1885
|
+
"model Anthropic stream first delta: model=%s ttfb_ms=%d",
|
|
1886
|
+
model.model,
|
|
1887
|
+
int((time.monotonic() - request_started) * 1000),
|
|
1888
|
+
)
|
|
1889
|
+
yield ModelStreamChunk(text=text, reasoning=reasoning, usage=usage_update)
|
|
1890
|
+
if event_type == "message_stop":
|
|
1891
|
+
if refused or stop_reason == "refusal":
|
|
1892
|
+
yield ModelStreamChunk(
|
|
1893
|
+
usage=usage_accumulator.report(), done=True,
|
|
1894
|
+
finish_reason="content_filter",
|
|
1895
|
+
)
|
|
1896
|
+
return
|
|
1897
|
+
try:
|
|
1898
|
+
text_tail, reasoning_tail = content_stream.finish()
|
|
1899
|
+
except StudioError as error:
|
|
1900
|
+
raise ModelResponseError(error, usage_accumulator.report()) from None
|
|
1901
|
+
if text_tail or reasoning_tail:
|
|
1902
|
+
raw_text += text_tail
|
|
1903
|
+
yield ModelStreamChunk(text=text_tail, reasoning=reasoning_tail)
|
|
1904
|
+
calls = tuple(
|
|
1905
|
+
ToolCall(
|
|
1906
|
+
id=value.call_id, name=value.name,
|
|
1907
|
+
arguments=value.arguments or "{}",
|
|
1908
|
+
)
|
|
1909
|
+
for value in tool_blocks.finish()
|
|
1910
|
+
)
|
|
1911
|
+
calls = _normalize_tool_call_arguments(calls, tools)
|
|
1912
|
+
finish_reason = {
|
|
1913
|
+
"end_turn": "stop",
|
|
1914
|
+
"stop_sequence": "stop",
|
|
1915
|
+
"max_tokens": "length",
|
|
1916
|
+
"tool_use": "tool_calls",
|
|
1917
|
+
"refusal": "content_filter",
|
|
1918
|
+
}.get(stop_reason, stop_reason)
|
|
1919
|
+
if refused:
|
|
1920
|
+
finish_reason, calls = "content_filter", ()
|
|
1921
|
+
if not raw_text.strip() and not calls and finish_reason != "content_filter":
|
|
1922
|
+
raise StudioError(
|
|
1923
|
+
"MODEL_EMPTY_RESPONSE",
|
|
1924
|
+
"模型未返回可用内容",
|
|
1925
|
+
status_code=502,
|
|
1926
|
+
details={"finishReason": finish_reason},
|
|
1927
|
+
)
|
|
1928
|
+
if not calls and raw_text.strip():
|
|
1929
|
+
_raise_for_repetitive_output(raw_text)
|
|
1930
|
+
usage_value = usage_accumulator.report()
|
|
1931
|
+
yield ModelStreamChunk(
|
|
1932
|
+
tool_calls=calls, usage=usage_value, done=True,
|
|
1933
|
+
finish_reason=finish_reason,
|
|
1934
|
+
provider_content=content_stream.replay_content(),
|
|
1935
|
+
)
|
|
1936
|
+
return
|
|
1937
|
+
|
|
1938
|
+
if refused or stop_reason == "refusal":
|
|
1939
|
+
yield ModelStreamChunk(
|
|
1940
|
+
usage=usage_accumulator.report(), done=True, finish_reason="content_filter",
|
|
1941
|
+
)
|
|
1942
|
+
|
|
469
1943
|
@staticmethod
|
|
470
1944
|
def _is_length_truncation(exc: StudioError) -> bool:
|
|
471
1945
|
if exc.code != "MODEL_EMPTY_RESPONSE":
|
|
@@ -474,11 +1948,145 @@ class OpenAICompatibleModelClient:
|
|
|
474
1948
|
|
|
475
1949
|
@staticmethod
|
|
476
1950
|
def _expand_length_budget(payload: dict[str, Any]) -> dict[str, Any]:
|
|
477
|
-
|
|
1951
|
+
if "max_output_tokens" in payload:
|
|
1952
|
+
key = "max_output_tokens"
|
|
1953
|
+
elif "max_completion_tokens" in payload:
|
|
1954
|
+
key = "max_completion_tokens"
|
|
1955
|
+
else:
|
|
1956
|
+
key = "max_tokens"
|
|
478
1957
|
current = int(payload.get(key) or 0)
|
|
479
1958
|
payload[key] = max(current, _LENGTH_RETRY_TARGET_TOKENS)
|
|
480
1959
|
return payload
|
|
481
1960
|
|
|
1961
|
+
@staticmethod
|
|
1962
|
+
def _apply_tool_choice(
|
|
1963
|
+
payload: dict[str, Any], wire_api: str, choice: str | dict[str, Any] | None,
|
|
1964
|
+
) -> None:
|
|
1965
|
+
"""Project explicit host admission without silently downgrading it.
|
|
1966
|
+
|
|
1967
|
+
Omitted choice preserves each wire's existing default. Unsupported
|
|
1968
|
+
provider policies are reported by the ordinary error path; this layer
|
|
1969
|
+
never retries with auto or changes a reasoning profile to force tools.
|
|
1970
|
+
"""
|
|
1971
|
+
if choice is None:
|
|
1972
|
+
return
|
|
1973
|
+
if isinstance(choice, dict):
|
|
1974
|
+
function = choice.get("function")
|
|
1975
|
+
if (set(choice) != {"type", "function"} or choice.get("type") != "function"
|
|
1976
|
+
or not isinstance(function, dict) or set(function) != {"name"}
|
|
1977
|
+
or not isinstance(function["name"], str) or not function["name"].strip()):
|
|
1978
|
+
raise ValueError("invalid named tool choice")
|
|
1979
|
+
name = function["name"]
|
|
1980
|
+
available = {
|
|
1981
|
+
tool.get("name") or (tool.get("function") or {}).get("name")
|
|
1982
|
+
for tool in payload.get("tools", ())
|
|
1983
|
+
}
|
|
1984
|
+
if name not in available:
|
|
1985
|
+
raise ValueError("named tool choice needs an advertised tool")
|
|
1986
|
+
payload["tool_choice"] = (
|
|
1987
|
+
{"type": "tool", "name": name} if wire_api == "anthropic"
|
|
1988
|
+
else {"type": "function", "name": name} if wire_api == "responses"
|
|
1989
|
+
else {"type": "function", "function": {"name": name}}
|
|
1990
|
+
)
|
|
1991
|
+
return
|
|
1992
|
+
if choice == "required" and not payload.get("tools"):
|
|
1993
|
+
raise ValueError("required tool choice needs an admitted tool")
|
|
1994
|
+
if wire_api == "anthropic":
|
|
1995
|
+
kinds = {"auto": "auto", "required": "any", "none": "none"}
|
|
1996
|
+
if choice not in kinds:
|
|
1997
|
+
raise ValueError("unsupported Anthropic tool choice")
|
|
1998
|
+
payload["tool_choice"] = {"type": kinds[choice]}
|
|
1999
|
+
else:
|
|
2000
|
+
payload["tool_choice"] = choice
|
|
2001
|
+
|
|
2002
|
+
@staticmethod
|
|
2003
|
+
def _apply_parallel_tool_policy(
|
|
2004
|
+
payload: dict[str, Any], wire_api: str, parallel: bool | None,
|
|
2005
|
+
) -> None:
|
|
2006
|
+
"""Preserve explicit generation policy independently of tool execution.
|
|
2007
|
+
|
|
2008
|
+
Omission keeps the existing wire defaults. This controls model output;
|
|
2009
|
+
it grants no permission to execute effects concurrently.
|
|
2010
|
+
"""
|
|
2011
|
+
if parallel is None or not payload.get("tools"):
|
|
2012
|
+
return
|
|
2013
|
+
if wire_api == "anthropic":
|
|
2014
|
+
choice = dict(payload.get("tool_choice") or {"type": "auto"})
|
|
2015
|
+
if choice.get("type") != "none":
|
|
2016
|
+
choice["disable_parallel_tool_use"] = not parallel
|
|
2017
|
+
payload["tool_choice"] = choice
|
|
2018
|
+
else:
|
|
2019
|
+
payload["parallel_tool_calls"] = parallel
|
|
2020
|
+
|
|
2021
|
+
@staticmethod
|
|
2022
|
+
def _apply_output_token_parameter(
|
|
2023
|
+
payload: dict[str, Any], wire_api: str,
|
|
2024
|
+
parameter: Literal["max_tokens", "max_completion_tokens"] | None,
|
|
2025
|
+
) -> None:
|
|
2026
|
+
"""Rename the Chat limit after budget projection; preserve its value.
|
|
2027
|
+
|
|
2028
|
+
Responses and Anthropic keep their native output-limit fields. No
|
|
2029
|
+
model-name inference, second cap, or adaptive request is introduced.
|
|
2030
|
+
"""
|
|
2031
|
+
if wire_api not in {"responses", "anthropic"} and parameter == "max_completion_tokens":
|
|
2032
|
+
if "max_tokens" in payload:
|
|
2033
|
+
payload["max_completion_tokens"] = payload.pop("max_tokens")
|
|
2034
|
+
|
|
2035
|
+
@staticmethod
|
|
2036
|
+
def _apply_thinking_policy(
|
|
2037
|
+
payload: dict[str, Any],
|
|
2038
|
+
wire_api: str,
|
|
2039
|
+
profile: provider_compat.CompatProfile,
|
|
2040
|
+
tools: list[dict[str, Any]] | None,
|
|
2041
|
+
effort: str | None,
|
|
2042
|
+
*, thinking_mode: Literal["disabled"] | None = None,
|
|
2043
|
+
) -> None:
|
|
2044
|
+
if wire_api not in {"chat", "anthropic"} or not profile.supports_thinking_field:
|
|
2045
|
+
return
|
|
2046
|
+
if thinking_mode == "disabled":
|
|
2047
|
+
payload.update(provider_compat.with_thinking_disabled(
|
|
2048
|
+
payload, profile, reasoning_effort=effort,
|
|
2049
|
+
))
|
|
2050
|
+
elif provider_compat.should_disable_thinking(
|
|
2051
|
+
profile, tools_present=bool(tools) and payload.get("tool_choice") not in (
|
|
2052
|
+
"none", {"type": "none"},
|
|
2053
|
+
), max_output_tokens=payload.get("max_tokens"),
|
|
2054
|
+
reasoning_effort=effort,
|
|
2055
|
+
):
|
|
2056
|
+
payload["thinking"] = {"type": "disabled"}
|
|
2057
|
+
elif wire_api == "anthropic" and profile.anthropic_block_binding == "drop_block":
|
|
2058
|
+
thinking = dict(payload.get("thinking") or {"type": "adaptive"})
|
|
2059
|
+
if thinking.get("type") in {"adaptive", "enabled"}:
|
|
2060
|
+
thinking["block_binding"] = {"prefix_mismatch_behavior": "drop_block"}
|
|
2061
|
+
payload["thinking"] = thinking
|
|
2062
|
+
|
|
2063
|
+
@staticmethod
|
|
2064
|
+
def _anthropic_headers(credential: str, payload: dict[str, Any]) -> dict[str, str]:
|
|
2065
|
+
headers = {
|
|
2066
|
+
"x-api-key": credential,
|
|
2067
|
+
"anthropic-version": ANTHROPIC_VERSION,
|
|
2068
|
+
"Content-Type": "application/json",
|
|
2069
|
+
}
|
|
2070
|
+
if "block_binding" in (payload.get("thinking") or {}):
|
|
2071
|
+
headers["anthropic-beta"] = "thinking-binding-controls-2026-08-01"
|
|
2072
|
+
return headers
|
|
2073
|
+
|
|
2074
|
+
@staticmethod
|
|
2075
|
+
def _apply_reasoning_effort(
|
|
2076
|
+
payload: dict[str, Any], wire_api: str, effort: str | None,
|
|
2077
|
+
) -> None:
|
|
2078
|
+
"""Project an already resolved effort without changing thinking mode or caps."""
|
|
2079
|
+
if effort is None:
|
|
2080
|
+
return
|
|
2081
|
+
if wire_api in {"responses", "anthropic"}:
|
|
2082
|
+
key = "reasoning" if wire_api == "responses" else "output_config"
|
|
2083
|
+
config = payload.get(key)
|
|
2084
|
+
config = dict(config) if isinstance(config, dict) else {}
|
|
2085
|
+
config["effort"] = effort
|
|
2086
|
+
payload[key] = config
|
|
2087
|
+
else:
|
|
2088
|
+
payload["reasoning_effort"] = effort
|
|
2089
|
+
|
|
482
2090
|
@staticmethod
|
|
483
2091
|
def _responses_payload(
|
|
484
2092
|
model: ResolvedModel,
|
|
@@ -486,144 +2094,130 @@ class OpenAICompatibleModelClient:
|
|
|
486
2094
|
tools: list[dict[str, Any]] | None,
|
|
487
2095
|
) -> dict[str, Any]:
|
|
488
2096
|
"""把 chat 语义消息映射到 Responses API 最小可用载荷。"""
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
)
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
if role == "system":
|
|
501
|
-
instructions.append(text)
|
|
502
|
-
continue
|
|
503
|
-
part_type = "output_text" if role == "assistant" else "input_text"
|
|
504
|
-
items.append({"role": role, "content": [{"type": part_type, "text": text}]})
|
|
2097
|
+
# Responses tools are flat objects, unlike Chat Completions' nested
|
|
2098
|
+
# ``function`` envelope. Accept both shapes because Harness supplies
|
|
2099
|
+
# Chat-style schemas internally.
|
|
2100
|
+
responses_tools = []
|
|
2101
|
+
for tool in tools or []:
|
|
2102
|
+
function = tool.get("function") if isinstance(tool, dict) else None
|
|
2103
|
+
if isinstance(function, dict):
|
|
2104
|
+
responses_tools.append({"type": "function", **function})
|
|
2105
|
+
elif isinstance(tool, dict):
|
|
2106
|
+
responses_tools.append(dict(tool))
|
|
2107
|
+
instructions, items = responses_messages(messages)
|
|
505
2108
|
payload: dict[str, Any] = {
|
|
506
2109
|
"model": model.model,
|
|
507
2110
|
"input": items,
|
|
508
2111
|
}
|
|
2112
|
+
if responses_tools:
|
|
2113
|
+
payload["tools"] = responses_tools
|
|
2114
|
+
payload["tool_choice"] = "auto"
|
|
2115
|
+
payload["parallel_tool_calls"] = True
|
|
509
2116
|
if model.parameters.max_tokens is not None:
|
|
510
2117
|
payload["max_output_tokens"] = model.parameters.max_tokens
|
|
2118
|
+
if model.parameters.temperature is not None:
|
|
2119
|
+
payload["temperature"] = model.parameters.temperature
|
|
2120
|
+
if model.parameters.top_p is not None:
|
|
2121
|
+
payload["top_p"] = model.parameters.top_p
|
|
511
2122
|
if instructions:
|
|
512
2123
|
payload["instructions"] = "\n\n".join(instructions)
|
|
513
2124
|
return payload
|
|
514
2125
|
|
|
515
2126
|
@staticmethod
|
|
516
|
-
def
|
|
517
|
-
response: httpx.Response, allow_empty: bool = False
|
|
2127
|
+
def _parse_buffered_response(
|
|
2128
|
+
response: httpx.Response, *, wire_api: str, allow_empty: bool = False,
|
|
518
2129
|
) -> ModelResponse:
|
|
519
2130
|
if len(response.content) > 16 * 1024 * 1024:
|
|
520
2131
|
raise StudioError(
|
|
521
|
-
"MODEL_RESPONSE_TOO_LARGE",
|
|
522
|
-
"模型响应超过 16 MiB 限制",
|
|
523
|
-
status_code=502,
|
|
2132
|
+
"MODEL_RESPONSE_TOO_LARGE", "模型响应超过 16 MiB 限制", status_code=502,
|
|
524
2133
|
)
|
|
2134
|
+
payload = decode_model_frame(response.content)
|
|
2135
|
+
validate_buffered_identity(payload, wire_api=wire_api)
|
|
2136
|
+
usage = provider_usage(payload.get("usage"), wire_api=wire_api)
|
|
525
2137
|
try:
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
"MODEL_RESPONSE_INVALID",
|
|
530
|
-
"模型响应不符合 Responses 协议",
|
|
531
|
-
status_code=502,
|
|
532
|
-
) from exc
|
|
533
|
-
content = payload.get("output_text")
|
|
534
|
-
if not isinstance(content, str) or not content:
|
|
535
|
-
parts: list[str] = []
|
|
536
|
-
for item in payload.get("output") or []:
|
|
537
|
-
if not isinstance(item, dict):
|
|
538
|
-
continue
|
|
539
|
-
for part in item.get("content") or []:
|
|
540
|
-
if isinstance(part, dict) and part.get("type") in {"output_text", "text"}:
|
|
541
|
-
parts.append(str(part.get("text") or ""))
|
|
542
|
-
content = "".join(parts)
|
|
543
|
-
status = str(payload.get("status") or "")
|
|
544
|
-
if not content and not allow_empty:
|
|
545
|
-
raise StudioError(
|
|
546
|
-
"MODEL_EMPTY_RESPONSE",
|
|
547
|
-
"模型未返回可用内容",
|
|
548
|
-
status_code=502,
|
|
549
|
-
details={"status": status, "finishReason": status},
|
|
2138
|
+
failure = provider_error(
|
|
2139
|
+
payload, wire_api=wire_api, usage=usage, headers=response.headers,
|
|
2140
|
+
provider_request_id=_provider_request_id(response.headers),
|
|
550
2141
|
)
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
2142
|
+
if failure is not None:
|
|
2143
|
+
raise failure
|
|
2144
|
+
parsed = buffered_projection(payload, wire_api=wire_api)
|
|
2145
|
+
except StudioError as error:
|
|
2146
|
+
raise ModelResponseError(error, usage) from None
|
|
2147
|
+
calls = [ToolCall(id=call.call_id, name=call.name, arguments=call.arguments, raw=call.raw)
|
|
2148
|
+
for call in parsed.calls]
|
|
2149
|
+
if (not parsed.content and not calls and not allow_empty
|
|
2150
|
+
and parsed.finish not in {"content_filter", "canceled", "cancelled"}):
|
|
2151
|
+
raise ModelResponseError(StudioError(
|
|
2152
|
+
"MODEL_EMPTY_RESPONSE", "模型未返回可用内容", status_code=502,
|
|
2153
|
+
details={"finishReason": parsed.finish},
|
|
2154
|
+
), usage)
|
|
563
2155
|
return ModelResponse(
|
|
564
|
-
content=content,
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
raw_message={"output": payload.get("output")},
|
|
2156
|
+
content=parsed.content, finish_reason=parsed.finish, usage=usage or Usage(),
|
|
2157
|
+
tool_calls=calls,
|
|
2158
|
+
raw_message=parsed.raw_message, reasoning=parsed.reasoning,
|
|
2159
|
+
provider_request_id=_provider_request_id(response.headers),
|
|
569
2160
|
)
|
|
570
2161
|
|
|
571
2162
|
@staticmethod
|
|
572
|
-
def
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
status_code=502,
|
|
578
|
-
)
|
|
579
|
-
try:
|
|
580
|
-
payload = response.json()
|
|
581
|
-
choice = payload["choices"][0]
|
|
582
|
-
message = choice["message"]
|
|
583
|
-
except (ValueError, KeyError, IndexError, TypeError) as exc:
|
|
584
|
-
raise StudioError(
|
|
585
|
-
"MODEL_RESPONSE_INVALID",
|
|
586
|
-
"模型响应不符合 OpenAI-compatible 协议",
|
|
587
|
-
status_code=502,
|
|
588
|
-
) from exc
|
|
589
|
-
calls = []
|
|
590
|
-
for raw in message.get("tool_calls") or []:
|
|
591
|
-
function = raw.get("function") or {}
|
|
592
|
-
calls.append(
|
|
593
|
-
ToolCall(
|
|
594
|
-
id=str(raw.get("id") or ""),
|
|
595
|
-
name=str(function.get("name") or ""),
|
|
596
|
-
arguments=str(function.get("arguments") or "{}"),
|
|
597
|
-
raw=raw,
|
|
598
|
-
)
|
|
599
|
-
)
|
|
600
|
-
content = str(message.get("content") or "")
|
|
601
|
-
finish_reason = str(choice.get("finish_reason") or "")
|
|
602
|
-
if not content and not calls and not allow_empty:
|
|
603
|
-
raise StudioError(
|
|
604
|
-
"MODEL_EMPTY_RESPONSE",
|
|
605
|
-
"模型未返回可用内容",
|
|
606
|
-
status_code=502,
|
|
607
|
-
details={"finishReason": finish_reason},
|
|
608
|
-
)
|
|
609
|
-
raw_usage = payload.get("usage") or {}
|
|
610
|
-
usage = Usage(
|
|
611
|
-
input_tokens=int(raw_usage.get("prompt_tokens") or 0),
|
|
612
|
-
output_tokens=int(raw_usage.get("completion_tokens") or 0),
|
|
613
|
-
total_tokens=int(raw_usage.get("total_tokens") or 0),
|
|
614
|
-
cached_input_tokens=int(
|
|
615
|
-
(raw_usage.get("prompt_tokens_details") or {}).get("cached_tokens") or 0
|
|
616
|
-
),
|
|
617
|
-
reasoning_output_tokens=int(
|
|
618
|
-
(raw_usage.get("completion_tokens_details") or {}).get("reasoning_tokens") or 0
|
|
619
|
-
),
|
|
620
|
-
reported=bool(raw_usage),
|
|
621
|
-
source="model-provider" if raw_usage else None,
|
|
2163
|
+
def _parse_responses_response(
|
|
2164
|
+
response: httpx.Response, allow_empty: bool = False,
|
|
2165
|
+
) -> ModelResponse:
|
|
2166
|
+
return OpenAICompatibleModelClient._parse_buffered_response(
|
|
2167
|
+
response, wire_api="responses", allow_empty=allow_empty,
|
|
622
2168
|
)
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
2169
|
+
|
|
2170
|
+
@staticmethod
|
|
2171
|
+
def _anthropic_payload(
|
|
2172
|
+
model: ResolvedModel,
|
|
2173
|
+
messages: list[dict[str, Any]],
|
|
2174
|
+
tools: list[dict[str, Any]] | None,
|
|
2175
|
+
*,
|
|
2176
|
+
compatibility_profile: provider_compat.CompatProfile | None = None,
|
|
2177
|
+
) -> dict[str, Any]:
|
|
2178
|
+
"""把 chat 语义消息映射到 Anthropic Messages API 最小可用载荷。"""
|
|
2179
|
+
profile = compatibility_profile or provider_compat.compat_profile(
|
|
2180
|
+
base_url=model.endpoint_url, model=model.model,
|
|
2181
|
+
)
|
|
2182
|
+
system_parts, merged = anthropic_messages(
|
|
2183
|
+
messages, mid_system=profile.anthropic_mid_system is True,
|
|
2184
|
+
)
|
|
2185
|
+
payload: dict[str, Any] = {
|
|
2186
|
+
"model": model.model,
|
|
2187
|
+
"messages": merged,
|
|
2188
|
+
# Anthropic Messages wire 必填 max_tokens;未配置 profile 参数时给安全默认。
|
|
2189
|
+
"max_tokens": model.parameters.max_tokens or _ANTHROPIC_DEFAULT_MAX_TOKENS,
|
|
2190
|
+
}
|
|
2191
|
+
if model.parameters.temperature is not None:
|
|
2192
|
+
payload["temperature"] = model.parameters.temperature
|
|
2193
|
+
if model.parameters.top_p is not None:
|
|
2194
|
+
payload["top_p"] = model.parameters.top_p
|
|
2195
|
+
if system_parts:
|
|
2196
|
+
payload["system"] = "\n\n".join(system_parts)
|
|
2197
|
+
anthropic_tools = []
|
|
2198
|
+
for tool in tools or []:
|
|
2199
|
+
function = tool.get("function") if isinstance(tool, dict) else None
|
|
2200
|
+
if not isinstance(function, dict):
|
|
2201
|
+
continue
|
|
2202
|
+
anthropic_tools.append({
|
|
2203
|
+
"name": str(function.get("name") or ""),
|
|
2204
|
+
"description": str(function.get("description") or ""),
|
|
2205
|
+
"input_schema": function.get("parameters") or {"type": "object", "properties": {}},
|
|
2206
|
+
})
|
|
2207
|
+
if anthropic_tools:
|
|
2208
|
+
payload["tools"] = anthropic_tools
|
|
2209
|
+
return payload
|
|
2210
|
+
|
|
2211
|
+
@staticmethod
|
|
2212
|
+
def _parse_anthropic_response(
|
|
2213
|
+
response: httpx.Response, allow_empty: bool = False,
|
|
2214
|
+
) -> ModelResponse:
|
|
2215
|
+
return OpenAICompatibleModelClient._parse_buffered_response(
|
|
2216
|
+
response, wire_api="anthropic", allow_empty=allow_empty,
|
|
2217
|
+
)
|
|
2218
|
+
|
|
2219
|
+
@staticmethod
|
|
2220
|
+
def _parse_response(response: httpx.Response, allow_empty: bool = False) -> ModelResponse:
|
|
2221
|
+
return OpenAICompatibleModelClient._parse_buffered_response(
|
|
2222
|
+
response, wire_api="chat", allow_empty=allow_empty,
|
|
629
2223
|
)
|