model-library-client 0.1.35__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- model_library_client-0.1.35/PKG-INFO +21 -0
- model_library_client-0.1.35/model_gateway/__init__.py +0 -0
- model_library_client-0.1.35/model_gateway/app.py +335 -0
- model_library_client-0.1.35/model_gateway/asgi_observability.py +145 -0
- model_library_client-0.1.35/model_gateway/auth.py +113 -0
- model_library_client-0.1.35/model_gateway/benchmark_admission_state.py +423 -0
- model_library_client-0.1.35/model_gateway/benchmark_admission_types.py +68 -0
- model_library_client-0.1.35/model_gateway/cache.py +58 -0
- model_library_client-0.1.35/model_gateway/capacity.py +348 -0
- model_library_client-0.1.35/model_gateway/errors.py +116 -0
- model_library_client-0.1.35/model_gateway/history.py +30 -0
- model_library_client-0.1.35/model_gateway/main.py +5 -0
- model_library_client-0.1.35/model_gateway/metrics.py +935 -0
- model_library_client-0.1.35/model_gateway/model_helpers.py +179 -0
- model_library_client-0.1.35/model_gateway/observability.py +172 -0
- model_library_client-0.1.35/model_gateway/rate_limit_monitor/__init__.py +6 -0
- model_library_client-0.1.35/model_gateway/rate_limit_monitor/manager.py +514 -0
- model_library_client-0.1.35/model_gateway/rate_limit_monitor/routes.py +64 -0
- model_library_client-0.1.35/model_gateway/rate_limit_monitor/state.py +678 -0
- model_library_client-0.1.35/model_gateway/rate_limit_monitor/types.py +173 -0
- model_library_client-0.1.35/model_gateway/route_helpers.py +242 -0
- model_library_client-0.1.35/model_gateway/routes/__init__.py +1 -0
- model_library_client-0.1.35/model_gateway/routes/benchmark_admission.py +182 -0
- model_library_client-0.1.35/model_gateway/routes/health.py +46 -0
- model_library_client-0.1.35/model_gateway/routes/models.py +98 -0
- model_library_client-0.1.35/model_gateway/routes/provider_ops.py +329 -0
- model_library_client-0.1.35/model_gateway/routes/query.py +297 -0
- model_library_client-0.1.35/model_gateway/routes/rate_limit.py +154 -0
- model_library_client-0.1.35/model_gateway/routes/run_tokens.py +83 -0
- model_library_client-0.1.35/model_gateway/routes/token_retry.py +55 -0
- model_library_client-0.1.35/model_gateway/run_token_types.py +83 -0
- model_library_client-0.1.35/model_gateway/run_tokens.py +153 -0
- model_library_client-0.1.35/model_gateway/runtime_config.py +570 -0
- model_library_client-0.1.35/model_gateway/startup_canary.py +127 -0
- model_library_client-0.1.35/model_gateway/telemetry_helpers.py +125 -0
- model_library_client-0.1.35/model_gateway/types.py +216 -0
- model_library_client-0.1.35/model_gateway/usage_ledger/__init__.py +1 -0
- model_library_client-0.1.35/model_gateway/usage_ledger/cost_alerts/__init__.py +1 -0
- model_library_client-0.1.35/model_gateway/usage_ledger/cost_alerts/handler.py +535 -0
- model_library_client-0.1.35/model_gateway/usage_ledger/cost_alerts/rules.py +863 -0
- model_library_client-0.1.35/model_gateway/usage_ledger/cost_alerts/slack.py +444 -0
- model_library_client-0.1.35/model_gateway/usage_ledger/details.py +106 -0
- model_library_client-0.1.35/model_gateway/usage_ledger/dynamodb_writer.py +224 -0
- model_library_client-0.1.35/model_gateway/usage_ledger/lambdas/__init__.py +1 -0
- model_library_client-0.1.35/model_gateway/usage_ledger/lambdas/writer.py +123 -0
- model_library_client-0.1.35/model_gateway/usage_ledger/message.py +97 -0
- model_library_client-0.1.35/model_gateway/usage_ledger/redshift_export.py +176 -0
- model_library_client-0.1.35/model_gateway/usage_ledger/redshift_export_job.py +477 -0
- model_library_client-0.1.35/model_gateway/usage_ledger/redshift_load.py +369 -0
- model_library_client-0.1.35/model_gateway/usage_ledger/redshift_schema.py +667 -0
- model_library_client-0.1.35/model_gateway/usage_ledger/redshift_transform.py +160 -0
- model_library_client-0.1.35/model_gateway/usage_ledger/schema.py +205 -0
- model_library_client-0.1.35/model_gateway/usage_ledger/store.py +1167 -0
- model_library_client-0.1.35/model_library/__init__.py +43 -0
- model_library_client-0.1.35/model_library/_licenses/openai-python.txt +201 -0
- model_library_client-0.1.35/model_library/agent/__init__.py +112 -0
- model_library_client-0.1.35/model_library/agent/__main__.py +3 -0
- model_library_client-0.1.35/model_library/agent/agent.py +1021 -0
- model_library_client-0.1.35/model_library/agent/cli.py +178 -0
- model_library_client-0.1.35/model_library/agent/conductor/__init__.py +15 -0
- model_library_client-0.1.35/model_library/agent/conductor/conductor.py +303 -0
- model_library_client-0.1.35/model_library/agent/conductor/config.py +9 -0
- model_library_client-0.1.35/model_library/agent/conductor/metadata.py +54 -0
- model_library_client-0.1.35/model_library/agent/config.py +230 -0
- model_library_client-0.1.35/model_library/agent/history_compaction.py +362 -0
- model_library_client-0.1.35/model_library/agent/hooks.py +196 -0
- model_library_client-0.1.35/model_library/agent/metadata.py +196 -0
- model_library_client-0.1.35/model_library/agent/tool.py +177 -0
- model_library_client-0.1.35/model_library/agent/tools/__init__.py +16 -0
- model_library_client-0.1.35/model_library/agent/tools/bash.py +219 -0
- model_library_client-0.1.35/model_library/agent/tools/stop.py +22 -0
- model_library_client-0.1.35/model_library/agent/tools/submit.py +37 -0
- model_library_client-0.1.35/model_library/agent/tools/web_search.py +138 -0
- model_library_client-0.1.35/model_library/atif.py +475 -0
- model_library_client-0.1.35/model_library/base/__init__.py +9 -0
- model_library_client-0.1.35/model_library/base/base.py +1186 -0
- model_library_client-0.1.35/model_library/base/delegate_only.py +200 -0
- model_library_client-0.1.35/model_library/base/gateway.py +751 -0
- model_library_client-0.1.35/model_library/base/input.py +325 -0
- model_library_client-0.1.35/model_library/base/json_schema.py +70 -0
- model_library_client-0.1.35/model_library/base/output/__init__.py +53 -0
- model_library_client-0.1.35/model_library/base/output/builder.py +419 -0
- model_library_client-0.1.35/model_library/base/output/moderation.py +80 -0
- model_library_client-0.1.35/model_library/base/output/performance.py +466 -0
- model_library_client-0.1.35/model_library/base/output/result.py +424 -0
- model_library_client-0.1.35/model_library/base/output/transcription.py +28 -0
- model_library_client-0.1.35/model_library/base/query_deadline.py +27 -0
- model_library_client-0.1.35/model_library/base/query_ids.py +149 -0
- model_library_client-0.1.35/model_library/base/query_logging.py +93 -0
- model_library_client-0.1.35/model_library/base/serialize.py +19 -0
- model_library_client-0.1.35/model_library/base/transcription.py +285 -0
- model_library_client-0.1.35/model_library/base/transcription_only.py +124 -0
- model_library_client-0.1.35/model_library/base/utils.py +101 -0
- model_library_client-0.1.35/model_library/config/README.md +84 -0
- model_library_client-0.1.35/model_library/config/ai21labs_models.yaml +22 -0
- model_library_client-0.1.35/model_library/config/alibaba_models.yaml +227 -0
- model_library_client-0.1.35/model_library/config/all_models.json +21003 -0
- model_library_client-0.1.35/model_library/config/amazon_models.yaml +139 -0
- model_library_client-0.1.35/model_library/config/ant_models.yaml +14 -0
- model_library_client-0.1.35/model_library/config/anthropic_models.yaml +546 -0
- model_library_client-0.1.35/model_library/config/arcee_models.yaml +27 -0
- model_library_client-0.1.35/model_library/config/azure_models.yaml +0 -0
- model_library_client-0.1.35/model_library/config/baseten_models.yaml +84 -0
- model_library_client-0.1.35/model_library/config/cohere_models.yaml +93 -0
- model_library_client-0.1.35/model_library/config/cursor_models.yaml +41 -0
- model_library_client-0.1.35/model_library/config/decision_models.yaml +80 -0
- model_library_client-0.1.35/model_library/config/deepseek_models.yaml +207 -0
- model_library_client-0.1.35/model_library/config/deprecated/ai21labs_models.yaml +113 -0
- model_library_client-0.1.35/model_library/config/deprecated/alibaba_models.yaml +280 -0
- model_library_client-0.1.35/model_library/config/deprecated/amazon_models.yaml +156 -0
- model_library_client-0.1.35/model_library/config/deprecated/ant_models.yaml +71 -0
- model_library_client-0.1.35/model_library/config/deprecated/anthropic_models.yaml +305 -0
- model_library_client-0.1.35/model_library/config/deprecated/arcee_models.yaml +57 -0
- model_library_client-0.1.35/model_library/config/deprecated/cohere_models.yaml +108 -0
- model_library_client-0.1.35/model_library/config/deprecated/fireworks_models.yaml +453 -0
- model_library_client-0.1.35/model_library/config/deprecated/google_models.yaml +537 -0
- model_library_client-0.1.35/model_library/config/deprecated/kimi_models.yaml +97 -0
- model_library_client-0.1.35/model_library/config/deprecated/mistral_models.yaml +337 -0
- model_library_client-0.1.35/model_library/config/deprecated/openai_models.yaml +523 -0
- model_library_client-0.1.35/model_library/config/deprecated/perplexity_models.yaml +166 -0
- model_library_client-0.1.35/model_library/config/deprecated/poolside_models.yaml +81 -0
- model_library_client-0.1.35/model_library/config/deprecated/together_models.yaml +927 -0
- model_library_client-0.1.35/model_library/config/deprecated/xai_models.yaml +105 -0
- model_library_client-0.1.35/model_library/config/deprecated/zai_models.yaml +403 -0
- model_library_client-0.1.35/model_library/config/devin_models.yaml +48 -0
- model_library_client-0.1.35/model_library/config/digitalocean_models.yaml +0 -0
- model_library_client-0.1.35/model_library/config/dummy_model.yaml +39 -0
- model_library_client-0.1.35/model_library/config/factory_models.yaml +44 -0
- model_library_client-0.1.35/model_library/config/fireworks_models.yaml +345 -0
- model_library_client-0.1.35/model_library/config/google_models.yaml +339 -0
- model_library_client-0.1.35/model_library/config/icons/AI21 Labs.svg +7 -0
- model_library_client-0.1.35/model_library/config/icons/AWS-dark.svg +8 -0
- model_library_client-0.1.35/model_library/config/icons/AWS.svg +8 -0
- model_library_client-0.1.35/model_library/config/icons/Alibaba-dark.svg +3 -0
- model_library_client-0.1.35/model_library/config/icons/Alibaba.svg +3 -0
- model_library_client-0.1.35/model_library/config/icons/Amazon-dark.svg +8 -0
- model_library_client-0.1.35/model_library/config/icons/Amazon.svg +8 -0
- model_library_client-0.1.35/model_library/config/icons/Ant Group-dark.svg +18 -0
- model_library_client-0.1.35/model_library/config/icons/Ant Group.svg +10 -0
- model_library_client-0.1.35/model_library/config/icons/Anthropic-dark.svg +4 -0
- model_library_client-0.1.35/model_library/config/icons/Anthropic.svg +4 -0
- model_library_client-0.1.35/model_library/config/icons/Arcee AI-dark.svg +6 -0
- model_library_client-0.1.35/model_library/config/icons/Arcee AI.svg +6 -0
- model_library_client-0.1.35/model_library/config/icons/AssemblyAI.svg +4 -0
- model_library_client-0.1.35/model_library/config/icons/Cartesia.svg +4 -0
- model_library_client-0.1.35/model_library/config/icons/Cohere-dark.svg +10 -0
- model_library_client-0.1.35/model_library/config/icons/Cohere.svg +12 -0
- model_library_client-0.1.35/model_library/config/icons/Cursor-dark.svg +12 -0
- model_library_client-0.1.35/model_library/config/icons/Cursor.svg +12 -0
- model_library_client-0.1.35/model_library/config/icons/DeepSeek-dark.svg +8 -0
- model_library_client-0.1.35/model_library/config/icons/DeepSeek.svg +10 -0
- model_library_client-0.1.35/model_library/config/icons/Deepgram.svg +10 -0
- model_library_client-0.1.35/model_library/config/icons/Devin-dark.svg +12 -0
- model_library_client-0.1.35/model_library/config/icons/Devin.svg +12 -0
- model_library_client-0.1.35/model_library/config/icons/ElevenLabs.svg +4 -0
- model_library_client-0.1.35/model_library/config/icons/Fireworks.png +0 -0
- model_library_client-0.1.35/model_library/config/icons/Google Cloud-dark.svg +11 -0
- model_library_client-0.1.35/model_library/config/icons/Google Cloud.svg +11 -0
- model_library_client-0.1.35/model_library/config/icons/Google-dark.svg +11 -0
- model_library_client-0.1.35/model_library/config/icons/Google.svg +11 -0
- model_library_client-0.1.35/model_library/config/icons/Groq.svg +1 -0
- model_library_client-0.1.35/model_library/config/icons/Inception-dark.svg +3 -0
- model_library_client-0.1.35/model_library/config/icons/Inception.svg +3 -0
- model_library_client-0.1.35/model_library/config/icons/Inworld.png +0 -0
- model_library_client-0.1.35/model_library/config/icons/Meta-dark.svg +26 -0
- model_library_client-0.1.35/model_library/config/icons/Meta.svg +34 -0
- model_library_client-0.1.35/model_library/config/icons/Microsoft.svg +1 -0
- model_library_client-0.1.35/model_library/config/icons/MiniMax AI-dark.svg +3 -0
- model_library_client-0.1.35/model_library/config/icons/MiniMax AI.svg +9 -0
- model_library_client-0.1.35/model_library/config/icons/MiniMax-dark.svg +3 -0
- model_library_client-0.1.35/model_library/config/icons/MiniMax.svg +9 -0
- model_library_client-0.1.35/model_library/config/icons/Mistral-dark.svg +22 -0
- model_library_client-0.1.35/model_library/config/icons/Mistral.svg +22 -0
- model_library_client-0.1.35/model_library/config/icons/Moonshot AI-dark.svg +3 -0
- model_library_client-0.1.35/model_library/config/icons/Moonshot AI.svg +3 -0
- model_library_client-0.1.35/model_library/config/icons/NVIDIA-dark.svg +3 -0
- model_library_client-0.1.35/model_library/config/icons/NVIDIA.svg +3 -0
- model_library_client-0.1.35/model_library/config/icons/OpenAI-dark.svg +10 -0
- model_library_client-0.1.35/model_library/config/icons/OpenAI.svg +10 -0
- model_library_client-0.1.35/model_library/config/icons/Poolside-dark.svg +1 -0
- model_library_client-0.1.35/model_library/config/icons/Poolside.svg +1 -0
- model_library_client-0.1.35/model_library/config/icons/Reson8.png +0 -0
- model_library_client-0.1.35/model_library/config/icons/Technology Innovation Institute.svg +4 -0
- model_library_client-0.1.35/model_library/config/icons/Tencent-dark.svg +1 -0
- model_library_client-0.1.35/model_library/config/icons/Tencent.svg +1 -0
- model_library_client-0.1.35/model_library/config/icons/Thinking Machines-dark.svg +4 -0
- model_library_client-0.1.35/model_library/config/icons/Thinking Machines.svg +4 -0
- model_library_client-0.1.35/model_library/config/icons/Xiaomi-dark.svg +10 -0
- model_library_client-0.1.35/model_library/config/icons/Xiaomi.svg +20 -0
- model_library_client-0.1.35/model_library/config/icons/xAI.png +0 -0
- model_library_client-0.1.35/model_library/config/icons/zAI-dark.svg +3 -0
- model_library_client-0.1.35/model_library/config/icons/zAI.svg +3 -0
- model_library_client-0.1.35/model_library/config/inception_models.yaml +40 -0
- model_library_client-0.1.35/model_library/config/kimi_models.yaml +102 -0
- model_library_client-0.1.35/model_library/config/meta_models.yaml +137 -0
- model_library_client-0.1.35/model_library/config/minimax_models.yaml +111 -0
- model_library_client-0.1.35/model_library/config/mistral_models.yaml +110 -0
- model_library_client-0.1.35/model_library/config/nvidia_models.yaml +10 -0
- model_library_client-0.1.35/model_library/config/openai_models.yaml +1059 -0
- model_library_client-0.1.35/model_library/config/openrouter_models.yaml +0 -0
- model_library_client-0.1.35/model_library/config/poolside_models.yaml +39 -0
- model_library_client-0.1.35/model_library/config/ramp_models.yaml +0 -0
- model_library_client-0.1.35/model_library/config/sakana_models.yaml +0 -0
- model_library_client-0.1.35/model_library/config/stepfun_models.yaml +31 -0
- model_library_client-0.1.35/model_library/config/tencent_models.yaml +29 -0
- model_library_client-0.1.35/model_library/config/thinkingmachines_models.yaml +10 -0
- model_library_client-0.1.35/model_library/config/thomsonreuters_models.yaml +21 -0
- model_library_client-0.1.35/model_library/config/together_models.yaml +133 -0
- model_library_client-0.1.35/model_library/config/vercel_models.yaml +29 -0
- model_library_client-0.1.35/model_library/config/voice/assemblyai_models.yaml +23 -0
- model_library_client-0.1.35/model_library/config/voice/aws_transcribe_models.yaml +23 -0
- model_library_client-0.1.35/model_library/config/voice/azure_speech_models.yaml +19 -0
- model_library_client-0.1.35/model_library/config/voice/cartesia_models.yaml +18 -0
- model_library_client-0.1.35/model_library/config/voice/deepgram_models.yaml +31 -0
- model_library_client-0.1.35/model_library/config/voice/elevenlabs_models.yaml +22 -0
- model_library_client-0.1.35/model_library/config/voice/google_cloud_models.yaml +23 -0
- model_library_client-0.1.35/model_library/config/voice/groq_models.yaml +30 -0
- model_library_client-0.1.35/model_library/config/voice/inworld_models.yaml +22 -0
- model_library_client-0.1.35/model_library/config/voice/reson8_models.yaml +33 -0
- model_library_client-0.1.35/model_library/config/xai_models.yaml +497 -0
- model_library_client-0.1.35/model_library/config/xiaomi_models.yaml +99 -0
- model_library_client-0.1.35/model_library/config/zai_models.yaml +68 -0
- model_library_client-0.1.35/model_library/custom_register_models.py +77 -0
- model_library_client-0.1.35/model_library/exceptions.py +469 -0
- model_library_client-0.1.35/model_library/failure_capture/__init__.py +1 -0
- model_library_client-0.1.35/model_library/failure_capture/aiohttp.py +133 -0
- model_library_client-0.1.35/model_library/failure_capture/botocore.py +94 -0
- model_library_client-0.1.35/model_library/failure_capture/core.py +451 -0
- model_library_client-0.1.35/model_library/failure_capture/httpx.py +83 -0
- model_library_client-0.1.35/model_library/file_utils.py +114 -0
- model_library_client-0.1.35/model_library/logging.py +50 -0
- model_library_client-0.1.35/model_library/model_utils.py +39 -0
- model_library_client-0.1.35/model_library/providers/__init__.py +0 -0
- model_library_client-0.1.35/model_library/providers/ai21labs.py +268 -0
- model_library_client-0.1.35/model_library/providers/amazon/__init__.py +3 -0
- model_library_client-0.1.35/model_library/providers/amazon/_credentials.py +17 -0
- model_library_client-0.1.35/model_library/providers/amazon/chat.py +515 -0
- model_library_client-0.1.35/model_library/providers/amazon/transcribe.py +156 -0
- model_library_client-0.1.35/model_library/providers/anthropic.py +995 -0
- model_library_client-0.1.35/model_library/providers/azure.py +98 -0
- model_library_client-0.1.35/model_library/providers/delegates/__init__.py +0 -0
- model_library_client-0.1.35/model_library/providers/delegates/alibaba.py +203 -0
- model_library_client-0.1.35/model_library/providers/delegates/ant.py +57 -0
- model_library_client-0.1.35/model_library/providers/delegates/arcee.py +35 -0
- model_library_client-0.1.35/model_library/providers/delegates/baseten.py +44 -0
- model_library_client-0.1.35/model_library/providers/delegates/cohere.py +45 -0
- model_library_client-0.1.35/model_library/providers/delegates/deepseek.py +55 -0
- model_library_client-0.1.35/model_library/providers/delegates/digitalocean.py +102 -0
- model_library_client-0.1.35/model_library/providers/delegates/fireworks.py +147 -0
- model_library_client-0.1.35/model_library/providers/delegates/inception.py +37 -0
- model_library_client-0.1.35/model_library/providers/delegates/kimi.py +184 -0
- model_library_client-0.1.35/model_library/providers/delegates/meta.py +96 -0
- model_library_client-0.1.35/model_library/providers/delegates/minimax.py +53 -0
- model_library_client-0.1.35/model_library/providers/delegates/nvidia.py +34 -0
- model_library_client-0.1.35/model_library/providers/delegates/openrouter/__init__.py +3 -0
- model_library_client-0.1.35/model_library/providers/delegates/openrouter/decision.py +341 -0
- model_library_client-0.1.35/model_library/providers/delegates/openrouter/openrouter.py +105 -0
- model_library_client-0.1.35/model_library/providers/delegates/perplexity.py +35 -0
- model_library_client-0.1.35/model_library/providers/delegates/poolside.py +36 -0
- model_library_client-0.1.35/model_library/providers/delegates/ramp.py +116 -0
- model_library_client-0.1.35/model_library/providers/delegates/sakana.py +31 -0
- model_library_client-0.1.35/model_library/providers/delegates/stepfun.py +68 -0
- model_library_client-0.1.35/model_library/providers/delegates/thinkingmachines.py +66 -0
- model_library_client-0.1.35/model_library/providers/delegates/thomsonreuters.py +86 -0
- model_library_client-0.1.35/model_library/providers/delegates/together.py +60 -0
- model_library_client-0.1.35/model_library/providers/delegates/vercel.py +37 -0
- model_library_client-0.1.35/model_library/providers/delegates/xiaomi.py +37 -0
- model_library_client-0.1.35/model_library/providers/delegates/zai.py +91 -0
- model_library_client-0.1.35/model_library/providers/google/__init__.py +3 -0
- model_library_client-0.1.35/model_library/providers/google/google.py +792 -0
- model_library_client-0.1.35/model_library/providers/google/speech.py +143 -0
- model_library_client-0.1.35/model_library/providers/google/transcribe.py +173 -0
- model_library_client-0.1.35/model_library/providers/mistral/__init__.py +4 -0
- model_library_client-0.1.35/model_library/providers/mistral/chat.py +481 -0
- model_library_client-0.1.35/model_library/providers/mistral/transcribe.py +143 -0
- model_library_client-0.1.35/model_library/providers/openai/__init__.py +11 -0
- model_library_client-0.1.35/model_library/providers/openai/_shared.py +136 -0
- model_library_client-0.1.35/model_library/providers/openai/completions.py +617 -0
- model_library_client-0.1.35/model_library/providers/openai/openai.py +320 -0
- model_library_client-0.1.35/model_library/providers/openai/responses.py +679 -0
- model_library_client-0.1.35/model_library/providers/openai/voice.py +286 -0
- model_library_client-0.1.35/model_library/providers/vals.py +226 -0
- model_library_client-0.1.35/model_library/providers/voice/__init__.py +1 -0
- model_library_client-0.1.35/model_library/providers/voice/assemblyai.py +204 -0
- model_library_client-0.1.35/model_library/providers/voice/azure_speech.py +156 -0
- model_library_client-0.1.35/model_library/providers/voice/cartesia.py +116 -0
- model_library_client-0.1.35/model_library/providers/voice/deepgram.py +216 -0
- model_library_client-0.1.35/model_library/providers/voice/elevenlabs.py +164 -0
- model_library_client-0.1.35/model_library/providers/voice/groq.py +58 -0
- model_library_client-0.1.35/model_library/providers/voice/inworld.py +163 -0
- model_library_client-0.1.35/model_library/providers/voice/meta.py +98 -0
- model_library_client-0.1.35/model_library/providers/voice/reson8.py +145 -0
- model_library_client-0.1.35/model_library/providers/voice/xai.py +141 -0
- model_library_client-0.1.35/model_library/providers/xai.py +576 -0
- model_library_client-0.1.35/model_library/py.typed +0 -0
- model_library_client-0.1.35/model_library/query_utils.py +85 -0
- model_library_client-0.1.35/model_library/rate_limits/__init__.py +21 -0
- model_library_client-0.1.35/model_library/rate_limits/model.py +241 -0
- model_library_client-0.1.35/model_library/rate_limits/probe.py +28 -0
- model_library_client-0.1.35/model_library/register_models.py +966 -0
- model_library_client-0.1.35/model_library/registry_utils.py +429 -0
- model_library_client-0.1.35/model_library/retriers/__init__.py +0 -0
- model_library_client-0.1.35/model_library/retriers/backoff.py +86 -0
- model_library_client-0.1.35/model_library/retriers/base.py +343 -0
- model_library_client-0.1.35/model_library/retriers/token/__init__.py +38 -0
- model_library_client-0.1.35/model_library/retriers/token/background.py +419 -0
- model_library_client-0.1.35/model_library/retriers/token/benchmark_admission.py +225 -0
- model_library_client-0.1.35/model_library/retriers/token/benchmark_admission_client.py +130 -0
- model_library_client-0.1.35/model_library/retriers/token/benchmark_queue.py +799 -0
- model_library_client-0.1.35/model_library/retriers/token/token.py +847 -0
- model_library_client-0.1.35/model_library/retriers/token/utils.py +978 -0
- model_library_client-0.1.35/model_library/retriers/utils.py +11 -0
- model_library_client-0.1.35/model_library/settings.py +68 -0
- model_library_client-0.1.35/model_library/telemetry.py +1193 -0
- model_library_client-0.1.35/model_library/utils.py +407 -0
- model_library_client-0.1.35/model_library_client.egg-info/PKG-INFO +21 -0
- model_library_client-0.1.35/model_library_client.egg-info/SOURCES.txt +322 -0
- model_library_client-0.1.35/model_library_client.egg-info/dependency_links.txt +1 -0
- model_library_client-0.1.35/model_library_client.egg-info/requires.txt +11 -0
- model_library_client-0.1.35/model_library_client.egg-info/scm_file_list.json +319 -0
- model_library_client-0.1.35/model_library_client.egg-info/scm_version.json +8 -0
- model_library_client-0.1.35/model_library_client.egg-info/top_level.txt +2 -0
- model_library_client-0.1.35/pyproject.toml +48 -0
- model_library_client-0.1.35/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: model-library-client
|
|
3
|
+
Version: 0.1.35
|
|
4
|
+
Summary: SDK-free Gateway client for model-library
|
|
5
|
+
Author-email: "Vals AI, Inc." <contact@vals.ai>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/plain
|
|
9
|
+
Requires-Dist: typing-extensions<5.0,>=4.15.0
|
|
10
|
+
Requires-Dist: pydantic<3.0,>=2.12.5
|
|
11
|
+
Requires-Dist: pyyaml<7.0,>=6.0.3
|
|
12
|
+
Requires-Dist: rich<15.0,>=14.3.3
|
|
13
|
+
Requires-Dist: httpx[http2]<1.0,>=0.28.0
|
|
14
|
+
Requires-Dist: backoff<3.0,>=2.2.1
|
|
15
|
+
Requires-Dist: redis<7.0,>=6.4.0
|
|
16
|
+
Requires-Dist: tiktoken<1.0,>=0.12.0
|
|
17
|
+
Requires-Dist: pillow<13.0,>=12.1.1
|
|
18
|
+
Requires-Dist: python-dotenv<2.0,>=1.0.0
|
|
19
|
+
Requires-Dist: aiohttp<4.0,>=3.13.5
|
|
20
|
+
|
|
21
|
+
Install model-library-client to use the model_library Gateway client without provider SDK dependencies. Install model-library for the full provider distribution.
|
|
File without changes
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""FastAPI app factory for the model gateway."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from collections.abc import Awaitable, Callable
|
|
8
|
+
from contextlib import asynccontextmanager, suppress
|
|
9
|
+
from functools import partial
|
|
10
|
+
from typing import Literal, cast
|
|
11
|
+
|
|
12
|
+
import redis.asyncio as async_redis
|
|
13
|
+
from dotenv import load_dotenv
|
|
14
|
+
from redis.exceptions import TimeoutError as RedisTimeoutError
|
|
15
|
+
from fastapi import FastAPI, Request
|
|
16
|
+
from fastapi.exceptions import RequestValidationError
|
|
17
|
+
from fastapi.responses import JSONResponse
|
|
18
|
+
from starlette.responses import Response
|
|
19
|
+
|
|
20
|
+
import model_library.telemetry as telemetry
|
|
21
|
+
from model_library import model_library_settings
|
|
22
|
+
from model_library.registry_utils import get_model_names
|
|
23
|
+
from model_library.retriers.token import set_redis_client
|
|
24
|
+
|
|
25
|
+
from model_gateway.asgi_observability import GatewayObservabilityMiddleware
|
|
26
|
+
from model_gateway.auth import create_auth_middleware
|
|
27
|
+
from model_gateway.cache import ModelCache
|
|
28
|
+
from model_gateway.capacity import GatewayCapacityLimiter, create_capacity_middleware
|
|
29
|
+
from model_gateway.errors import ErrorBody, ErrorResponse, RunTokenAuthorizationError
|
|
30
|
+
from model_gateway.metrics import (
|
|
31
|
+
MetricPublisher,
|
|
32
|
+
create_metrics_middleware,
|
|
33
|
+
install_telemetry_delivery_metric_handler,
|
|
34
|
+
publish_metrics_periodically,
|
|
35
|
+
record_runtime,
|
|
36
|
+
remove_telemetry_delivery_metric_handler,
|
|
37
|
+
)
|
|
38
|
+
from model_gateway.observability import (
|
|
39
|
+
install_loop_exception_handler,
|
|
40
|
+
log_process_lifecycle,
|
|
41
|
+
runtime_snapshot,
|
|
42
|
+
)
|
|
43
|
+
from model_gateway.rate_limit_monitor import RateLimitMonitor
|
|
44
|
+
from model_gateway.rate_limit_monitor.routes import register_rate_limit_monitor_routes
|
|
45
|
+
from model_gateway.rate_limit_monitor.state import MonitorRedis, RateLimitMonitorStore
|
|
46
|
+
from model_gateway.routes.benchmark_admission import register_benchmark_admission_routes
|
|
47
|
+
from model_gateway.routes.health import register_health_routes
|
|
48
|
+
from model_gateway.routes.models import register_model_routes
|
|
49
|
+
from model_gateway.routes.provider_ops import register_provider_ops_routes
|
|
50
|
+
from model_gateway.routes.rate_limit import (
|
|
51
|
+
RateLimitProbeService,
|
|
52
|
+
register_rate_limit_route,
|
|
53
|
+
)
|
|
54
|
+
from model_gateway.routes.query import register_query_routes
|
|
55
|
+
from model_gateway.routes.run_tokens import register_run_token_routes
|
|
56
|
+
from model_gateway.routes.token_retry import register_token_retry_routes
|
|
57
|
+
from model_gateway.runtime_config import (
|
|
58
|
+
RuntimeConfigState,
|
|
59
|
+
runtime_config_manager_from_env,
|
|
60
|
+
)
|
|
61
|
+
from model_gateway.startup_canary import run_startup_canary, startup_canary_state
|
|
62
|
+
from model_gateway.telemetry_helpers import error_telemetry_attributes
|
|
63
|
+
from model_gateway.usage_ledger.store import (
|
|
64
|
+
NoopUsageLedger,
|
|
65
|
+
create_usage_ledger_from_env,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
logger = logging.getLogger("model_proxy_server")
|
|
69
|
+
|
|
70
|
+
load_dotenv()
|
|
71
|
+
|
|
72
|
+
# The server must never proxy to itself — clear the client-side gateway URL.
|
|
73
|
+
model_library_settings.unset("MODEL_GATEWAY_URL")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
GatewayRuntimeRole = Literal["combined", "query", "control"]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def env_flag(name: str, *, default: bool = False) -> bool:
|
|
80
|
+
value = os.environ.get(name)
|
|
81
|
+
if value is None:
|
|
82
|
+
return default
|
|
83
|
+
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def gateway_runtime_role() -> GatewayRuntimeRole:
|
|
87
|
+
value = os.environ.get("GATEWAY_RUNTIME_ROLE", "combined").strip().lower()
|
|
88
|
+
if value not in {"combined", "query", "control"}:
|
|
89
|
+
raise ValueError("GATEWAY_RUNTIME_ROLE must be combined, query, or control")
|
|
90
|
+
return cast(GatewayRuntimeRole, value)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def _record_runtime_current(loop: asyncio.AbstractEventLoop) -> None:
|
|
94
|
+
lag_start = loop.time()
|
|
95
|
+
await asyncio.sleep(0)
|
|
96
|
+
event_loop_lag_ms = int((loop.time() - lag_start) * 1000)
|
|
97
|
+
snapshot = runtime_snapshot()
|
|
98
|
+
snapshot["event_loop_lag_ms"] = event_loop_lag_ms
|
|
99
|
+
record_runtime(snapshot)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def create_app() -> FastAPI:
|
|
103
|
+
runtime_role = gateway_runtime_role()
|
|
104
|
+
query_enabled = runtime_role in {"combined", "query"}
|
|
105
|
+
control_enabled = runtime_role in {"combined", "control"}
|
|
106
|
+
api_keys = model_library_settings.get("MODEL_GATEWAY_API_KEYS", None)
|
|
107
|
+
if not isinstance(api_keys, str) or not api_keys:
|
|
108
|
+
raise RuntimeError("MODEL_GATEWAY_API_KEYS must be set")
|
|
109
|
+
api_keys_by_name = cast(dict[str, str], json.loads(api_keys))
|
|
110
|
+
valid_keys = set(api_keys_by_name.values())
|
|
111
|
+
|
|
112
|
+
hmac_secret_value = model_library_settings.get("MODEL_GATEWAY_HMAC_SECRET", None)
|
|
113
|
+
if not isinstance(hmac_secret_value, str) or not hmac_secret_value:
|
|
114
|
+
raise RuntimeError("MODEL_GATEWAY_HMAC_SECRET must be set")
|
|
115
|
+
hmac_secret = hmac_secret_value.encode()
|
|
116
|
+
cache = ModelCache()
|
|
117
|
+
runtime_config_state = RuntimeConfigState.bootstrap()
|
|
118
|
+
runtime_config_manager = runtime_config_manager_from_env(runtime_config_state)
|
|
119
|
+
capacity_limiter = GatewayCapacityLimiter()
|
|
120
|
+
startup_canary_enabled = query_enabled and env_flag(
|
|
121
|
+
"GATEWAY_STARTUP_CANARY_ENABLED"
|
|
122
|
+
)
|
|
123
|
+
usage_ledger = (
|
|
124
|
+
create_usage_ledger_from_env() if query_enabled else NoopUsageLedger()
|
|
125
|
+
)
|
|
126
|
+
rate_limit_probe_service: RateLimitProbeService | None = None
|
|
127
|
+
|
|
128
|
+
@asynccontextmanager
|
|
129
|
+
async def lifespan(app: FastAPI):
|
|
130
|
+
redis_url = os.environ.get("REDIS_URL", "")
|
|
131
|
+
if control_enabled and not redis_url:
|
|
132
|
+
raise RuntimeError("REDIS_URL must be set for a control-enabled runtime")
|
|
133
|
+
|
|
134
|
+
loop = asyncio.get_running_loop()
|
|
135
|
+
previous_exception_handler = install_loop_exception_handler(loop)
|
|
136
|
+
redis_client: async_redis.Redis | None = None
|
|
137
|
+
if redis_url:
|
|
138
|
+
redis_client = async_redis.from_url( # pyright: ignore[reportUnknownMemberType]
|
|
139
|
+
redis_url,
|
|
140
|
+
decode_responses=True,
|
|
141
|
+
socket_timeout=30,
|
|
142
|
+
socket_connect_timeout=10,
|
|
143
|
+
socket_keepalive=True,
|
|
144
|
+
health_check_interval=30,
|
|
145
|
+
retry_on_error=[RedisTimeoutError],
|
|
146
|
+
max_connections=200,
|
|
147
|
+
)
|
|
148
|
+
set_redis_client(redis_client)
|
|
149
|
+
|
|
150
|
+
metrics_stop = asyncio.Event()
|
|
151
|
+
metrics_publishers: list[MetricPublisher] = [
|
|
152
|
+
partial(_record_runtime_current, loop)
|
|
153
|
+
]
|
|
154
|
+
if query_enabled:
|
|
155
|
+
metrics_publishers.insert(0, capacity_limiter.record_current)
|
|
156
|
+
metrics_task: asyncio.Task[None] | None = None
|
|
157
|
+
usage_ledger_started = False
|
|
158
|
+
canary_task: asyncio.Task[None] | None = None
|
|
159
|
+
rate_limit_monitor: RateLimitMonitor | None = None
|
|
160
|
+
runtime_refresh_stop: asyncio.Event | None = None
|
|
161
|
+
runtime_refresh_task: asyncio.Task[None] | None = None
|
|
162
|
+
delivery_metric_handler = install_telemetry_delivery_metric_handler()
|
|
163
|
+
try:
|
|
164
|
+
if runtime_config_manager is not None:
|
|
165
|
+
initial_refresh = await runtime_config_manager.refresh_once()
|
|
166
|
+
if initial_refresh.failed or initial_refresh.rejected:
|
|
167
|
+
raise RuntimeError("Initial runtime configuration refresh failed")
|
|
168
|
+
runtime_refresh_stop = asyncio.Event()
|
|
169
|
+
runtime_refresh_task = asyncio.create_task(
|
|
170
|
+
runtime_config_manager.run(runtime_refresh_stop)
|
|
171
|
+
)
|
|
172
|
+
with runtime_config_state.capture():
|
|
173
|
+
model_count = len(get_model_names())
|
|
174
|
+
logger.info("Loaded gateway model registry with %s models", model_count)
|
|
175
|
+
log_process_lifecycle("gateway.process.startup")
|
|
176
|
+
telemetry.configure_telemetry(app)
|
|
177
|
+
if control_enabled:
|
|
178
|
+
assert redis_client is not None
|
|
179
|
+
rate_limit_monitor = RateLimitMonitor(
|
|
180
|
+
RateLimitMonitorStore(
|
|
181
|
+
cast(MonitorRedis, redis_client),
|
|
182
|
+
stage=os.environ.get("GATEWAY_STAGE", "local"),
|
|
183
|
+
),
|
|
184
|
+
runtime_capture=runtime_config_state.capture,
|
|
185
|
+
)
|
|
186
|
+
app.state.rate_limit_monitor = rate_limit_monitor
|
|
187
|
+
rate_limit_monitor.start()
|
|
188
|
+
metrics_task = asyncio.create_task(
|
|
189
|
+
publish_metrics_periodically(
|
|
190
|
+
metrics_stop,
|
|
191
|
+
publishers=metrics_publishers,
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
if startup_canary_enabled:
|
|
195
|
+
canary_task = asyncio.create_task(
|
|
196
|
+
run_startup_canary(app, next(iter(api_keys_by_name.values())))
|
|
197
|
+
)
|
|
198
|
+
await usage_ledger.start()
|
|
199
|
+
usage_ledger_started = True
|
|
200
|
+
yield
|
|
201
|
+
finally:
|
|
202
|
+
log_process_lifecycle("gateway.process.shutdown_start")
|
|
203
|
+
if runtime_refresh_stop is not None:
|
|
204
|
+
runtime_refresh_stop.set()
|
|
205
|
+
if runtime_refresh_task is not None:
|
|
206
|
+
await runtime_refresh_task
|
|
207
|
+
if runtime_config_manager is not None:
|
|
208
|
+
await runtime_config_manager.aclose()
|
|
209
|
+
if canary_task is not None and not canary_task.done():
|
|
210
|
+
canary_task.cancel()
|
|
211
|
+
with suppress(asyncio.CancelledError):
|
|
212
|
+
await canary_task
|
|
213
|
+
if rate_limit_probe_service is not None:
|
|
214
|
+
try:
|
|
215
|
+
await rate_limit_probe_service.close()
|
|
216
|
+
except Exception:
|
|
217
|
+
logger.exception(
|
|
218
|
+
"Gateway rate-limit probe service close failed during shutdown"
|
|
219
|
+
)
|
|
220
|
+
if usage_ledger_started:
|
|
221
|
+
try:
|
|
222
|
+
await usage_ledger.close()
|
|
223
|
+
except Exception:
|
|
224
|
+
logger.exception(
|
|
225
|
+
"Gateway usage ledger close failed during shutdown"
|
|
226
|
+
)
|
|
227
|
+
if rate_limit_monitor is not None:
|
|
228
|
+
try:
|
|
229
|
+
await rate_limit_monitor.close()
|
|
230
|
+
except Exception:
|
|
231
|
+
logger.exception(
|
|
232
|
+
"Gateway rate-limit monitor close failed during shutdown"
|
|
233
|
+
)
|
|
234
|
+
finally:
|
|
235
|
+
app.state.rate_limit_monitor = None
|
|
236
|
+
if redis_client is not None:
|
|
237
|
+
try:
|
|
238
|
+
await redis_client.aclose()
|
|
239
|
+
except Exception:
|
|
240
|
+
logger.exception("Gateway Redis close failed during shutdown")
|
|
241
|
+
try:
|
|
242
|
+
telemetry.shutdown_telemetry()
|
|
243
|
+
finally:
|
|
244
|
+
try:
|
|
245
|
+
remove_telemetry_delivery_metric_handler(delivery_metric_handler)
|
|
246
|
+
finally:
|
|
247
|
+
metrics_stop.set()
|
|
248
|
+
try:
|
|
249
|
+
if metrics_task is not None:
|
|
250
|
+
await metrics_task
|
|
251
|
+
finally:
|
|
252
|
+
loop.set_exception_handler(previous_exception_handler)
|
|
253
|
+
log_process_lifecycle("gateway.process.shutdown_done")
|
|
254
|
+
|
|
255
|
+
app = FastAPI(
|
|
256
|
+
title="Model Proxy",
|
|
257
|
+
lifespan=lifespan,
|
|
258
|
+
redirect_slashes=False,
|
|
259
|
+
docs_url=None if runtime_role == "control" else "/docs",
|
|
260
|
+
redoc_url=None if runtime_role == "control" else "/redoc",
|
|
261
|
+
openapi_url=None if runtime_role == "control" else "/openapi.json",
|
|
262
|
+
)
|
|
263
|
+
app.state.cache = cache
|
|
264
|
+
app.state.runtime_config_state = runtime_config_state
|
|
265
|
+
app.state.hmac_secret = hmac_secret
|
|
266
|
+
app.state.runtime_role = runtime_role
|
|
267
|
+
app.state.capacity_limiter = capacity_limiter
|
|
268
|
+
app.state.usage_ledger = usage_ledger
|
|
269
|
+
app.state.rate_limit_monitor = None
|
|
270
|
+
app.state.startup_canary = startup_canary_state(startup_canary_enabled)
|
|
271
|
+
|
|
272
|
+
@app.middleware("http")
|
|
273
|
+
async def capture_runtime_configuration(
|
|
274
|
+
request: Request,
|
|
275
|
+
call_next: Callable[[Request], Awaitable[Response]],
|
|
276
|
+
) -> Response:
|
|
277
|
+
with runtime_config_state.capture():
|
|
278
|
+
return await call_next(request)
|
|
279
|
+
|
|
280
|
+
if query_enabled:
|
|
281
|
+
app.middleware("http")(create_capacity_middleware())
|
|
282
|
+
app.middleware("http")(create_metrics_middleware())
|
|
283
|
+
app.middleware("http")(create_auth_middleware(api_keys_by_name))
|
|
284
|
+
app.add_middleware(GatewayObservabilityMiddleware)
|
|
285
|
+
|
|
286
|
+
@app.exception_handler(RunTokenAuthorizationError)
|
|
287
|
+
async def run_token_authorization_handler(
|
|
288
|
+
request: Request, exc: RunTokenAuthorizationError
|
|
289
|
+
) -> JSONResponse:
|
|
290
|
+
claims = request.state.run_token_claims
|
|
291
|
+
telemetry.add_event(
|
|
292
|
+
"gateway.run_token.violation",
|
|
293
|
+
{
|
|
294
|
+
"gateway.run_token.run_id": claims.run_id,
|
|
295
|
+
"gateway.run_token.task_id": claims.task_id,
|
|
296
|
+
"gateway.error.message": str(exc),
|
|
297
|
+
},
|
|
298
|
+
)
|
|
299
|
+
return JSONResponse(
|
|
300
|
+
status_code=403,
|
|
301
|
+
content={"code": "model_not_authorized", "message": str(exc)},
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
@app.exception_handler(RequestValidationError)
|
|
305
|
+
async def validation_exception_handler(
|
|
306
|
+
request: Request, exc: RequestValidationError
|
|
307
|
+
) -> JSONResponse:
|
|
308
|
+
err = ErrorResponse(
|
|
309
|
+
400,
|
|
310
|
+
ErrorBody(code="invalid_request", message=str(exc)),
|
|
311
|
+
)
|
|
312
|
+
error_attrs = error_telemetry_attributes(err, phase="request_validation")
|
|
313
|
+
telemetry.set_attributes(error_attrs)
|
|
314
|
+
telemetry.set_status_error(err.body.code)
|
|
315
|
+
telemetry.add_event("gateway.request_validation.error", error_attrs)
|
|
316
|
+
return JSONResponse(status_code=err.status_code, content=err.body.model_dump())
|
|
317
|
+
|
|
318
|
+
register_health_routes(
|
|
319
|
+
app,
|
|
320
|
+
valid_keys=valid_keys,
|
|
321
|
+
hmac_secret=hmac_secret,
|
|
322
|
+
control_enabled=control_enabled,
|
|
323
|
+
)
|
|
324
|
+
if control_enabled:
|
|
325
|
+
register_benchmark_admission_routes(app, cache=cache)
|
|
326
|
+
register_run_token_routes(app)
|
|
327
|
+
register_rate_limit_monitor_routes(app)
|
|
328
|
+
if control_enabled:
|
|
329
|
+
register_model_routes(app)
|
|
330
|
+
if query_enabled:
|
|
331
|
+
register_token_retry_routes(app)
|
|
332
|
+
register_query_routes(app, cache=cache)
|
|
333
|
+
rate_limit_probe_service = register_rate_limit_route(app, cache=cache)
|
|
334
|
+
register_provider_ops_routes(app, cache=cache)
|
|
335
|
+
return app
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""ASGI-level gateway request/response boundary diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
|
10
|
+
|
|
11
|
+
from model_gateway.observability import (
|
|
12
|
+
log_gateway_event,
|
|
13
|
+
request_log_fields_from_scope,
|
|
14
|
+
runtime_snapshot,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GatewayObservabilityMiddleware:
|
|
19
|
+
"""Capture request and ASGI response-boundary events without payload data."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
22
|
+
self.app = app
|
|
23
|
+
|
|
24
|
+
async def __call__(
|
|
25
|
+
self,
|
|
26
|
+
scope: Scope,
|
|
27
|
+
receive: Receive,
|
|
28
|
+
send: Send,
|
|
29
|
+
) -> None:
|
|
30
|
+
if scope.get("type") != "http":
|
|
31
|
+
await self.app(scope, receive, send)
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
path = cast(str, scope["path"])
|
|
35
|
+
method = cast(str, scope["method"])
|
|
36
|
+
request_fields = request_log_fields_from_scope(scope)
|
|
37
|
+
state = _RequestBoundaryState(start=time.perf_counter())
|
|
38
|
+
|
|
39
|
+
def log_anomaly(
|
|
40
|
+
event: str, *, level: int = logging.INFO, **fields: object
|
|
41
|
+
) -> None:
|
|
42
|
+
log_gateway_event(
|
|
43
|
+
event,
|
|
44
|
+
level=level,
|
|
45
|
+
route=path,
|
|
46
|
+
method=method,
|
|
47
|
+
runtime=_runtime_snapshot_for_anomaly(state),
|
|
48
|
+
**request_fields,
|
|
49
|
+
**fields,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
async def wrapped_receive() -> Message:
|
|
53
|
+
message = await receive()
|
|
54
|
+
if message["type"] == "http.disconnect" and not state.client_disconnected:
|
|
55
|
+
state.client_disconnected = True
|
|
56
|
+
log_anomaly(
|
|
57
|
+
"gateway.request.client_disconnect",
|
|
58
|
+
response_started=state.response_started,
|
|
59
|
+
)
|
|
60
|
+
return message
|
|
61
|
+
|
|
62
|
+
async def wrapped_send(message: Message) -> None:
|
|
63
|
+
message_type = message["type"]
|
|
64
|
+
try:
|
|
65
|
+
await send(message)
|
|
66
|
+
except Exception as exc:
|
|
67
|
+
state.send_error = True
|
|
68
|
+
log_anomaly(
|
|
69
|
+
"gateway.response.send_error",
|
|
70
|
+
level=logging.WARNING,
|
|
71
|
+
exception_type=type(exc).__name__,
|
|
72
|
+
response_started=state.response_started,
|
|
73
|
+
first_body_sent=state.first_body_sent,
|
|
74
|
+
status_code=state.status_code,
|
|
75
|
+
)
|
|
76
|
+
raise
|
|
77
|
+
if message_type == "http.response.start":
|
|
78
|
+
state.response_started = True
|
|
79
|
+
state.status_code = cast(int, message["status"])
|
|
80
|
+
elif message_type == "http.response.body":
|
|
81
|
+
if not state.first_body_sent:
|
|
82
|
+
state.first_body_sent = True
|
|
83
|
+
more_body = bool(message.get("more_body"))
|
|
84
|
+
if not more_body:
|
|
85
|
+
state.response_done = True
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
await self.app(scope, wrapped_receive, wrapped_send)
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
if not state.send_error:
|
|
91
|
+
log_anomaly(
|
|
92
|
+
"gateway.request.error",
|
|
93
|
+
level=logging.WARNING,
|
|
94
|
+
exception_type=type(exc).__name__,
|
|
95
|
+
response_started=state.response_started,
|
|
96
|
+
first_body_sent=state.first_body_sent,
|
|
97
|
+
status_code=state.status_code,
|
|
98
|
+
)
|
|
99
|
+
raise
|
|
100
|
+
finally:
|
|
101
|
+
duration_ms = (time.perf_counter() - state.start) * 1000
|
|
102
|
+
if not state.response_started:
|
|
103
|
+
log_anomaly(
|
|
104
|
+
"gateway.response.missing_start",
|
|
105
|
+
level=logging.WARNING,
|
|
106
|
+
client_disconnected=state.client_disconnected,
|
|
107
|
+
)
|
|
108
|
+
if _is_anomaly(state):
|
|
109
|
+
log_anomaly(
|
|
110
|
+
"gateway.request.done",
|
|
111
|
+
status_code=state.status_code,
|
|
112
|
+
duration_ms=duration_ms,
|
|
113
|
+
response_started=state.response_started,
|
|
114
|
+
first_body_sent=state.first_body_sent,
|
|
115
|
+
response_done=state.response_done,
|
|
116
|
+
send_error=state.send_error,
|
|
117
|
+
client_disconnected=state.client_disconnected,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _runtime_snapshot_for_anomaly(state: "_RequestBoundaryState") -> dict[str, int]:
|
|
122
|
+
if state.anomaly_runtime is None:
|
|
123
|
+
state.anomaly_runtime = runtime_snapshot()
|
|
124
|
+
return state.anomaly_runtime
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _is_anomaly(state: "_RequestBoundaryState") -> bool:
|
|
128
|
+
return (
|
|
129
|
+
state.send_error
|
|
130
|
+
or state.client_disconnected
|
|
131
|
+
or not state.response_done
|
|
132
|
+
or (state.status_code is not None and not 200 <= state.status_code < 300)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class _RequestBoundaryState:
|
|
137
|
+
def __init__(self, *, start: float):
|
|
138
|
+
self.start = start
|
|
139
|
+
self.response_started = False
|
|
140
|
+
self.first_body_sent = False
|
|
141
|
+
self.response_done = False
|
|
142
|
+
self.send_error = False
|
|
143
|
+
self.client_disconnected = False
|
|
144
|
+
self.status_code: int | None = None
|
|
145
|
+
self.anomaly_runtime: dict[str, int] | None = None
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Bearer token auth middleware."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
|
|
7
|
+
from fastapi import Request
|
|
8
|
+
from fastapi.responses import JSONResponse
|
|
9
|
+
|
|
10
|
+
import model_library.telemetry as telemetry
|
|
11
|
+
from starlette.middleware.base import RequestResponseEndpoint
|
|
12
|
+
from starlette.responses import Response
|
|
13
|
+
|
|
14
|
+
from model_gateway.run_token_types import RUN_TOKEN_OPERATION_PATHS
|
|
15
|
+
from model_gateway.run_tokens import RUN_TOKEN_PREFIX, get_run_token, sha256_hex
|
|
16
|
+
|
|
17
|
+
EXEMPT_PATHS = {"/health/live", "/health/ready"}
|
|
18
|
+
TRACE_AUTH_FAILURE_PATHS = telemetry.HTTP_TRACE_ALLOWED_ROUTES
|
|
19
|
+
|
|
20
|
+
RUN_TOKEN_ALLOWED_PATHS = frozenset({*RUN_TOKEN_OPERATION_PATHS.values(), "/registry"})
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_auth_middleware(api_keys_by_name: Mapping[str, str]):
|
|
24
|
+
# Pre-hash keys for constant-time comparison (prevents timing side-channel)
|
|
25
|
+
hashed_keys = tuple(
|
|
26
|
+
(name, hashlib.sha256(api_key.encode()).digest())
|
|
27
|
+
for name, api_key in api_keys_by_name.items()
|
|
28
|
+
)
|
|
29
|
+
accepted_digests = {digest.hex() for _, digest in hashed_keys}
|
|
30
|
+
|
|
31
|
+
async def auth_middleware(
|
|
32
|
+
request: Request, call_next: RequestResponseEndpoint
|
|
33
|
+
) -> Response:
|
|
34
|
+
if request.url.path in EXEMPT_PATHS:
|
|
35
|
+
return await call_next(request)
|
|
36
|
+
|
|
37
|
+
auth_header = request.headers.get("Authorization", "")
|
|
38
|
+
if not auth_header.startswith("Bearer "):
|
|
39
|
+
return _unauthorized_response(
|
|
40
|
+
request,
|
|
41
|
+
"Missing or malformed Authorization header",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
presented = auth_header[7:]
|
|
45
|
+
request.state.run_token_claims = None
|
|
46
|
+
|
|
47
|
+
if presented.startswith(RUN_TOKEN_PREFIX):
|
|
48
|
+
# Before the lookup, so a denied path cannot leak token validity.
|
|
49
|
+
if request.url.path not in RUN_TOKEN_ALLOWED_PATHS:
|
|
50
|
+
return _auth_error_response(
|
|
51
|
+
request, 403, "forbidden", "Run tokens cannot call this route"
|
|
52
|
+
)
|
|
53
|
+
claims = await get_run_token(sha256_hex(presented))
|
|
54
|
+
# Dev and prod share Redis but not keys.
|
|
55
|
+
if claims is None or claims.minted_with not in accepted_digests:
|
|
56
|
+
return _unauthorized_response(request, "Invalid or expired run token")
|
|
57
|
+
if claims.allowed_operations is not None and request.url.path not in {
|
|
58
|
+
"/registry",
|
|
59
|
+
*(RUN_TOKEN_OPERATION_PATHS[op] for op in claims.allowed_operations),
|
|
60
|
+
}:
|
|
61
|
+
return _auth_error_response(
|
|
62
|
+
request, 403, "forbidden", "Run token does not allow this operation"
|
|
63
|
+
)
|
|
64
|
+
request.state.gateway_api_key_name = f"run-token:{claims.minted_by}"
|
|
65
|
+
request.state.gateway_api_key_digest = claims.minted_with
|
|
66
|
+
request.state.run_token_claims = claims
|
|
67
|
+
return await call_next(request)
|
|
68
|
+
|
|
69
|
+
token_hash = hashlib.sha256(presented.encode()).digest()
|
|
70
|
+
matched_name: str | None = None
|
|
71
|
+
for name, expected_hash in hashed_keys:
|
|
72
|
+
if hmac.compare_digest(token_hash, expected_hash):
|
|
73
|
+
matched_name = name
|
|
74
|
+
if matched_name is None:
|
|
75
|
+
return _unauthorized_response(request, "Invalid API key")
|
|
76
|
+
|
|
77
|
+
request.state.gateway_api_key_name = matched_name
|
|
78
|
+
request.state.gateway_api_key_digest = token_hash.hex()
|
|
79
|
+
return await call_next(request)
|
|
80
|
+
|
|
81
|
+
return auth_middleware
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _unauthorized_response(request: Request, message: str) -> JSONResponse:
|
|
85
|
+
return _auth_error_response(request, 401, "unauthorized", message)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _auth_error_response(
|
|
89
|
+
request: Request, status_code: int, code: str, message: str
|
|
90
|
+
) -> JSONResponse:
|
|
91
|
+
attrs = {
|
|
92
|
+
"gateway.route": request.url.path,
|
|
93
|
+
"gateway.operation": "access_check",
|
|
94
|
+
"gateway.error.code": "access_denied",
|
|
95
|
+
"gateway.error.phase": "access_control",
|
|
96
|
+
"gateway.status_code": status_code,
|
|
97
|
+
"http.request.method": request.method,
|
|
98
|
+
"http.status_code": status_code,
|
|
99
|
+
"http.response.status_code": status_code,
|
|
100
|
+
}
|
|
101
|
+
if request.url.path in TRACE_AUTH_FAILURE_PATHS:
|
|
102
|
+
with telemetry.start_span(
|
|
103
|
+
telemetry.http_server_span_name(request.method, request.url.path),
|
|
104
|
+
attrs,
|
|
105
|
+
kind="server",
|
|
106
|
+
):
|
|
107
|
+
telemetry.set_attributes(attrs)
|
|
108
|
+
telemetry.set_status_error("access_denied")
|
|
109
|
+
telemetry.add_event("gateway.auth.error", attrs)
|
|
110
|
+
return JSONResponse(
|
|
111
|
+
status_code=status_code,
|
|
112
|
+
content={"code": code, "message": message},
|
|
113
|
+
)
|