mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-12 08:42:18 +00:00
Merge remote-tracking branch 'origin/main' into litellm_ollama_model_info_api_base
Some checks failed
Cost map pins recheck / rerun-cost-map-guard (push) Has been cancelled
LiteLLM Rust / rust-lint (push) Has been cancelled
LiteLLM Rust / rust-test (push) Has been cancelled
LiteLLM Rust DB / db-queries (push) Has been cancelled
LiteLLM Rust / rust-wheel (push) Has been cancelled
Some checks failed
Cost map pins recheck / rerun-cost-map-guard (push) Has been cancelled
LiteLLM Rust / rust-lint (push) Has been cancelled
LiteLLM Rust / rust-test (push) Has been cancelled
LiteLLM Rust DB / db-queries (push) Has been cancelled
LiteLLM Rust / rust-wheel (push) Has been cancelled
# Conflicts: # tests/integration/providers/test_decisions_wire.py
This commit is contained in:
commit
757b7df388
361 changed files with 26253 additions and 5134 deletions
|
|
@ -128,6 +128,8 @@ fi
|
|||
sudo iptables -L integration_only -n -v -x > "$results/egress-guard.txt"
|
||||
awk '$3 == "REJECT" && $1 > 0 { rejected=1 } END { exit !rejected }' "$results/egress-guard.txt"
|
||||
|
||||
export UV_OFFLINE=true
|
||||
|
||||
if [ "$mode" = parity ]; then
|
||||
export INTEGRATION_WORKERS=1
|
||||
else
|
||||
|
|
|
|||
8
.github/PULL_REQUEST_TEMPLATE/general.md
vendored
8
.github/PULL_REQUEST_TEMPLATE/general.md
vendored
|
|
@ -9,10 +9,16 @@
|
|||
<!-- The PR author rewrites this in their own words before asking for review. Being able to sum up the PR in a few
|
||||
plain sentences proves the author understands what it does, and human-written technical English is far easier
|
||||
for a reviewer to parse than AI-written English
|
||||
An AI agent may draft a starting point here, but the author replaces it with their own handwritten version
|
||||
An AI agent may draft a starting point here, but always wraps its draft in the make-me-human-readable tags below,
|
||||
backticks included so the tags stay visible on GitHub, so nobody mistakes it for the author's words. The author rewrites it in their own words and deletes the tags.
|
||||
Tags still present at review time mean the summary is AI-written and has not been rewritten yet
|
||||
Say what the PR does for users and why, then anything that keeps it working over time (a sync job, a config someone owns)
|
||||
No bullets, tables, or code names here. Everything else goes inside the Details block below -->
|
||||
|
||||
`<make-me-human-readable>`
|
||||
<blah>
|
||||
`</make-me-human-readable>`
|
||||
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
||||
|
|
|
|||
30
.github/workflows/deploy-dev-on-main.yml
vendored
30
.github/workflows/deploy-dev-on-main.yml
vendored
|
|
@ -1,30 +0,0 @@
|
|||
name: Deploy main to gateway-dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: deploy-dev-dispatch
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
dispatch:
|
||||
if: github.repository == 'BerriAI/litellm'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2
|
||||
id: token
|
||||
with:
|
||||
app-id: ${{ secrets.LITELLM_OPS_DISPATCH_APP_ID }}
|
||||
private-key: ${{ secrets.LITELLM_OPS_DISPATCH_APP_PRIVATE_KEY }}
|
||||
owner: BerriAI
|
||||
repositories: litellm-ops
|
||||
permission-actions: write
|
||||
- name: Dispatch the litellm-ops dev deploy
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.token.outputs.token }}
|
||||
run: gh api --method POST repos/BerriAI/litellm-ops/actions/workflows/deploy-dev.yml/dispatches -f ref=main
|
||||
85
.github/workflows/weekly_load_anomaly.yml
vendored
85
.github/workflows/weekly_load_anomaly.yml
vendored
|
|
@ -1,85 +0,0 @@
|
|||
name: "Weekly Load Anomaly Check"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 12 * * 6"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
weekly-load-anomaly:
|
||||
if: github.event_name != 'schedule' || github.repository == 'BerriAI/litellm'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16.6
|
||||
env:
|
||||
POSTGRES_USER: llmproxy
|
||||
POSTGRES_PASSWORD: dbpassword9090
|
||||
POSTGRES_DB: litellm
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U llmproxy"
|
||||
--health-interval 5s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
env:
|
||||
DATABASE_URL: postgresql://llmproxy:dbpassword9090@localhost:5432/litellm
|
||||
LITELLM_MASTER_KEY: sk-weekly-anomaly-check
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
AWS_BEARER_TOKEN_BEDROCK: ${{ secrets.AWS_BEARER_TOKEN_BEDROCK }}
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Set up uv
|
||||
uses: ./.github/actions/setup-uv-with-retries
|
||||
with:
|
||||
version: "0.10.9"
|
||||
|
||||
- name: Cache the Rust build
|
||||
uses: ./.github/actions/cache-cargo-build
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
.github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra proxy
|
||||
|
||||
- name: Cache Prisma binaries
|
||||
uses: ./.github/actions/cache-prisma-binaries
|
||||
|
||||
- name: Generate Prisma client
|
||||
run: |
|
||||
uv run --no-sync prisma generate --schema litellm/proxy/schema.prisma
|
||||
|
||||
- name: Start the proxy
|
||||
run: |
|
||||
nohup uv run --no-sync litellm --config tests/e2e/perf/weekly_anomaly_config.yml --port 4000 > proxy.log 2>&1 &
|
||||
for _ in $(seq 1 90); do
|
||||
if curl -fs http://localhost:4000/health/liveliness > /dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "proxy never became live"
|
||||
tail -n 100 proxy.log
|
||||
exit 1
|
||||
|
||||
- name: Run the weekly session anomaly test
|
||||
env:
|
||||
E2E_WEEKLY_ANOMALY: "1"
|
||||
run: |
|
||||
uv run --no-sync pytest tests/e2e/perf/test_weekly_session_anomaly_e2e.py -v --tb=short -rA
|
||||
|
||||
- name: Show proxy log on failure
|
||||
if: failure()
|
||||
run: tail -n 300 proxy.log
|
||||
|
|
@ -41,6 +41,8 @@ Same applies for filing bug reports and feature requests, with .github/ISSUE_TEM
|
|||
|
||||
If you're resolving a linear ticket, in the "## Linear ticket" section of the PR, say "Resolves LIT-1234", replacing "LIT-1234" with the actual ticket id that you're resolving. If you don't have the ticket id, don't make one up or search for it. Just drop the section
|
||||
|
||||
Run QA proxies with `python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml --num_workers 2 --detailed_debug --reload --use_v2_migration_resolver 2>&1 | tee litellm.log`, and swap its `--config` for a copy of `litellm/proxy/dev_config.yaml` that points both `router_settings` (`redis_host`, `redis_port`) and `general_settings.coordination_redis` (`host`, `port`) at one dedicated Redis instance, with credentials when required, so the workers share router state and spend, budget, and rate-limit counters. Check that two worker PIDs are serving, and use the same worker count and Redis setup for the before and after proof. When the PR is about `--run_gunicorn`, `--run_hypercorn`, or `--run_granian`, which `--reload` does not support, drop `--reload`. When it is about behavior without Redis, drop Redis. When the proxy prints `"workers" flag is ignored when reloading is enabled`, that checkout or its uvicorn reloads only one worker, so drop the two-worker check for it. Say in the proof which part you dropped and why
|
||||
|
||||
Simple PRs may skip before/after e2e when the full diff is mechanical and focused checks completely prove it, such as #49030 restoring an existing helper name. Trace affected callers and run relevant tests/static checks at the tip. For a bug fix, establish that the regression catches the original defect. Size alone never qualifies. New behavior, API/provider semantics, auth, billing, persistence, concurrency or uncertain integration risk need live e2e. In Screenshots / Proof of Fix, record `E2E exception at <tip SHA>`, scope, rationale, commands and results. Tests count as proof only for this exception. Reassess changed diffs, retain CI/bot gates, and honor explicit e2e requests (2026-10-28, unnecessary live QA for #49030)
|
||||
|
||||
For other PRs, proof must show the end user's real flow before and after, with commands and output against a live proxy and real provider calls where applicable. Pytest alone is insufficient. For UI or interactive coding-tool flows, drive that surface and embed before/after screenshots, with URLs, clicks and input fields a reviewer can reproduce
|
||||
|
|
|
|||
|
|
@ -696,7 +696,7 @@ For companies that need better security, user management and professional suppor
|
|||
[Talk to founders](https://enterprise.litellm.ai/demo)
|
||||
|
||||
This covers:
|
||||
- ✅ **Features under the [LiteLLM Commercial License](https://docs.litellm.ai/docs/proxy/enterprise):**
|
||||
- ✅ **Features under the [LiteLLM Commercial License](https://docs.litellm.ai/docs/enterprise):**
|
||||
- ✅ **Feature Prioritization**
|
||||
- ✅ **Custom Integrations**
|
||||
- ✅ **Professional Support - Dedicated discord + slack**
|
||||
|
|
|
|||
|
|
@ -124,12 +124,86 @@ pub enum AnthropicBeta {
|
|||
ToolSearchTool20251019,
|
||||
#[strum(serialize = "inline-tools-2026-09-15")]
|
||||
InlineTools20260915,
|
||||
#[strum(serialize = "afk-mode-2026-01-31")]
|
||||
AfkMode20260131,
|
||||
#[strum(serialize = "agent-memory-2026-07-22")]
|
||||
AgentMemory20260722,
|
||||
#[strum(serialize = "analytics-usage-cost-2026-08-24")]
|
||||
AnalyticsUsageCost20260824,
|
||||
#[strum(serialize = "cache-diagnosis-2026-04-07")]
|
||||
CacheDiagnosis20260407,
|
||||
#[strum(serialize = "ce-plugins-2026-09-01")]
|
||||
CePlugins20260901,
|
||||
#[strum(serialize = "ce-user-management-2026-07-13")]
|
||||
CeUserManagement20260713,
|
||||
#[strum(serialize = "code-execution-2025-05-22")]
|
||||
CodeExecution20250522,
|
||||
#[strum(serialize = "computer-use-2024-10-22")]
|
||||
ComputerUse20241022,
|
||||
#[strum(serialize = "dev-full-thinking-2025-05-14")]
|
||||
DevFullThinking20250514,
|
||||
#[strum(serialize = "dreaming-2026-04-21")]
|
||||
Dreaming20260421,
|
||||
#[strum(serialize = "experimental-claude-tag-2026-10-21")]
|
||||
ExperimentalClaudeTag20261021,
|
||||
#[strum(serialize = "extended-cache-ttl-2025-04-11")]
|
||||
ExtendedCacheTtl20250411,
|
||||
#[strum(serialize = "fallback-credit-2026-06-01")]
|
||||
FallbackCredit20260601,
|
||||
#[strum(serialize = "fallback-credit-2026-07-01")]
|
||||
FallbackCredit20260701,
|
||||
#[strum(serialize = "managed-agents-2026-04-01")]
|
||||
ManagedAgents20260401,
|
||||
#[strum(serialize = "mcp-client-2026-09-15")]
|
||||
McpClient20260915,
|
||||
#[strum(serialize = "mcp-tunnels-2026-06-22")]
|
||||
McpTunnels20260622,
|
||||
#[strum(serialize = "message-batches-2024-09-24")]
|
||||
MessageBatches20240924,
|
||||
#[strum(serialize = "message-threads-2026-08-12")]
|
||||
MessageThreads20260812,
|
||||
#[strum(serialize = "mid-conversation-system-clear-at-2026-08-21")]
|
||||
MidConversationSystemClearAt20260821,
|
||||
#[strum(serialize = "model-context-window-exceeded-2025-08-26")]
|
||||
ModelContextWindowExceeded20250826,
|
||||
#[strum(serialize = "output-300k-2026-03-24")]
|
||||
Output300k20260324,
|
||||
#[strum(serialize = "pdfs-2024-09-25")]
|
||||
Pdfs20240925,
|
||||
#[strum(serialize = "prompt-caching-2024-07-31")]
|
||||
PromptCaching20240731,
|
||||
#[strum(serialize = "redact-thinking-2026-02-12")]
|
||||
RedactThinking20260212,
|
||||
#[strum(serialize = "server-side-fallback-2026-06-01")]
|
||||
ServerSideFallback20260601,
|
||||
#[strum(serialize = "server-side-fallback-2026-07-01")]
|
||||
ServerSideFallback20260701,
|
||||
#[strum(serialize = "spend-limit-reads-2026-09-26")]
|
||||
SpendLimitReads20260926,
|
||||
#[strum(serialize = "structured-outputs-2025-12-15")]
|
||||
StructuredOutputs20251215,
|
||||
#[strum(serialize = "task-budgets-2026-03-13")]
|
||||
TaskBudgets20260313,
|
||||
#[strum(serialize = "telemetry-destinations-2026-08-11")]
|
||||
TelemetryDestinations20260811,
|
||||
#[strum(serialize = "thinking-token-count-2026-05-13")]
|
||||
ThinkingTokenCount20260513,
|
||||
#[strum(serialize = "token-counting-2024-11-01")]
|
||||
TokenCounting20241101,
|
||||
#[strum(serialize = "user-profiles-2026-03-24")]
|
||||
UserProfiles20260324,
|
||||
#[strum(serialize = "user-profiles-2026-08-18")]
|
||||
UserProfiles20260818,
|
||||
#[strum(serialize = "user-profiles-2026-09-04")]
|
||||
UserProfiles20260904,
|
||||
#[strum(serialize = "webhooks-2026-10-07")]
|
||||
Webhooks20261007,
|
||||
#[strum(default, transparent)]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl AnthropicBeta {
|
||||
pub const KNOWN: [Self; 40] = [
|
||||
pub const KNOWN: [Self; 77] = [
|
||||
Self::Oauth20250420,
|
||||
Self::WebFetch20250910,
|
||||
Self::WebSearch20250305,
|
||||
|
|
@ -170,6 +244,43 @@ impl AnthropicBeta {
|
|||
Self::ToolExamples20251029,
|
||||
Self::ToolSearchTool20251019,
|
||||
Self::InlineTools20260915,
|
||||
Self::AfkMode20260131,
|
||||
Self::AgentMemory20260722,
|
||||
Self::AnalyticsUsageCost20260824,
|
||||
Self::CacheDiagnosis20260407,
|
||||
Self::CePlugins20260901,
|
||||
Self::CeUserManagement20260713,
|
||||
Self::CodeExecution20250522,
|
||||
Self::ComputerUse20241022,
|
||||
Self::DevFullThinking20250514,
|
||||
Self::Dreaming20260421,
|
||||
Self::ExperimentalClaudeTag20261021,
|
||||
Self::ExtendedCacheTtl20250411,
|
||||
Self::FallbackCredit20260601,
|
||||
Self::FallbackCredit20260701,
|
||||
Self::ManagedAgents20260401,
|
||||
Self::McpClient20260915,
|
||||
Self::McpTunnels20260622,
|
||||
Self::MessageBatches20240924,
|
||||
Self::MessageThreads20260812,
|
||||
Self::MidConversationSystemClearAt20260821,
|
||||
Self::ModelContextWindowExceeded20250826,
|
||||
Self::Output300k20260324,
|
||||
Self::Pdfs20240925,
|
||||
Self::PromptCaching20240731,
|
||||
Self::RedactThinking20260212,
|
||||
Self::ServerSideFallback20260601,
|
||||
Self::ServerSideFallback20260701,
|
||||
Self::SpendLimitReads20260926,
|
||||
Self::StructuredOutputs20251215,
|
||||
Self::TaskBudgets20260313,
|
||||
Self::TelemetryDestinations20260811,
|
||||
Self::ThinkingTokenCount20260513,
|
||||
Self::TokenCounting20241101,
|
||||
Self::UserProfiles20260324,
|
||||
Self::UserProfiles20260818,
|
||||
Self::UserProfiles20260904,
|
||||
Self::Webhooks20261007,
|
||||
];
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
|
|
@ -195,10 +306,9 @@ impl AnthropicBeta {
|
|||
|
||||
fn rejected_by(&self) -> &'static [BetaProvider] {
|
||||
match self {
|
||||
Self::AdvancedToolUse20251120 => {
|
||||
Self::AdvancedToolUse20251120 | Self::ToolSearchTool20251019 => {
|
||||
&[BetaProvider::BedrockConverse, BetaProvider::Databricks]
|
||||
}
|
||||
Self::ContextManagement20250627 => &[BetaProvider::BedrockConverse],
|
||||
Self::AdvisorTool20260301 => &[
|
||||
BetaProvider::AzureAi,
|
||||
BetaProvider::BedrockConverse,
|
||||
|
|
@ -207,98 +317,99 @@ impl AnthropicBeta {
|
|||
BetaProvider::VertexAi,
|
||||
BetaProvider::Databricks,
|
||||
],
|
||||
Self::Compact20260904 => &[
|
||||
Self::AfkMode20260131
|
||||
| Self::CePlugins20260901
|
||||
| Self::CeUserManagement20260713
|
||||
| Self::Compact20260904
|
||||
| Self::Dreaming20260421
|
||||
| Self::ExperimentalClaudeTag20261021
|
||||
| Self::ExtendedCacheTtl20250411
|
||||
| Self::FallbackCredit20260601
|
||||
| Self::FallbackCredit20260701
|
||||
| Self::InlineTools20260915
|
||||
| Self::ManagedAgents20260401
|
||||
| Self::McpTunnels20260622
|
||||
| Self::MidConversationSystemClearAt20260821
|
||||
| Self::ModelContextWindowExceeded20250826
|
||||
| Self::Oauth20250420
|
||||
| Self::Pdfs20240925
|
||||
| Self::PromptCaching20240731
|
||||
| Self::PromptCachingScope20260105
|
||||
| Self::RedactThinking20260212
|
||||
| Self::ServerSideFallback20260601
|
||||
| Self::SpendLimitReads20260926
|
||||
| Self::StructuredOutputs20251215
|
||||
| Self::TaskBudgets20260313
|
||||
| Self::TelemetryDestinations20260811
|
||||
| Self::ThinkingTokenCount20260513
|
||||
| Self::TokenCounting20241101
|
||||
| Self::UserProfiles20260324
|
||||
| Self::UserProfiles20260818
|
||||
| Self::UserProfiles20260904 => &[
|
||||
BetaProvider::BedrockConverse,
|
||||
BetaProvider::Bedrock,
|
||||
BetaProvider::BedrockMantle,
|
||||
BetaProvider::Databricks,
|
||||
],
|
||||
Self::Bash20241022
|
||||
Self::AgentMemory20260722
|
||||
| Self::Bash20241022
|
||||
| Self::Bash20250124
|
||||
| Self::DevFullThinking20250514
|
||||
| Self::McpServers20251204
|
||||
| Self::StructuredOutput20240301
|
||||
| Self::TextEditor20241022
|
||||
| Self::TextEditor20250124 => BetaProvider::VARIANTS,
|
||||
Self::ClaudeCode20250219 | Self::ToolExamples20251029 => &[
|
||||
BetaProvider::Anthropic,
|
||||
BetaProvider::AzureAi,
|
||||
BetaProvider::BedrockConverse,
|
||||
BetaProvider::Bedrock,
|
||||
BetaProvider::VertexAi,
|
||||
BetaProvider::Databricks,
|
||||
],
|
||||
Self::CodeExecution20250825
|
||||
Self::AnalyticsUsageCost20260824
|
||||
| Self::CacheDiagnosis20260407
|
||||
| Self::CodeExecution20250522
|
||||
| Self::CodeExecution20250825
|
||||
| Self::ComputerUse20241022
|
||||
| Self::FastMode20260201
|
||||
| Self::FilesApi20250414
|
||||
| Self::McpClient20250404
|
||||
| Self::McpClient20251120
|
||||
| Self::PromptCachingScope20260105
|
||||
| Self::McpClient20260915
|
||||
| Self::MessageBatches20240924
|
||||
| Self::MessageThreads20260812
|
||||
| Self::Output300k20260324
|
||||
| Self::ServerSideFallback20260701
|
||||
| Self::Skills20251002
|
||||
| Self::WebFetch20250910 => &[
|
||||
| Self::WebFetch20250910
|
||||
| Self::Webhooks20261007 => &[
|
||||
BetaProvider::BedrockConverse,
|
||||
BetaProvider::Bedrock,
|
||||
BetaProvider::BedrockMantle,
|
||||
BetaProvider::VertexAi,
|
||||
BetaProvider::Databricks,
|
||||
],
|
||||
Self::Compact20260112 => &[BetaProvider::AzureAi, BetaProvider::BedrockConverse],
|
||||
Self::ClaudeCode20250219
|
||||
| Self::PerTurnControl20260701
|
||||
| Self::ToolExamples20251029 => &[
|
||||
BetaProvider::BedrockConverse,
|
||||
BetaProvider::Bedrock,
|
||||
BetaProvider::Databricks,
|
||||
],
|
||||
Self::Compact20260112
|
||||
| Self::ContextManagement20250627
|
||||
| Self::DangerousToolUse20260903 => &[BetaProvider::BedrockConverse],
|
||||
Self::ComputerUse20250124
|
||||
| Self::ComputerUse20251124
|
||||
| Self::Context1m20250807
|
||||
| Self::Effort20251124
|
||||
| Self::FineGrainedToolStreaming20250514
|
||||
| Self::ThinkingBindingControls20260801
|
||||
| Self::ThinkingDisplayUpdates20260818 => &[],
|
||||
Self::DangerousToolUse20260903 => &[BetaProvider::BedrockConverse],
|
||||
Self::Effort20251124 => &[BetaProvider::VertexAi],
|
||||
Self::FastMode20260201 | Self::Oauth20250420 => &[
|
||||
BetaProvider::AzureAi,
|
||||
BetaProvider::BedrockConverse,
|
||||
BetaProvider::Bedrock,
|
||||
BetaProvider::BedrockMantle,
|
||||
BetaProvider::VertexAi,
|
||||
BetaProvider::Databricks,
|
||||
],
|
||||
Self::FineGrainedToolStreaming20250514 => {
|
||||
&[BetaProvider::AzureAi, BetaProvider::VertexAi]
|
||||
}
|
||||
Self::InterleavedThinking20250514 | Self::WebSearch20250305 => {
|
||||
&[BetaProvider::BedrockConverse, BetaProvider::Bedrock]
|
||||
}
|
||||
Self::MidConversationOutputConfig20260701 => &[
|
||||
BetaProvider::AzureAi,
|
||||
BetaProvider::BedrockConverse,
|
||||
BetaProvider::BedrockMantle,
|
||||
BetaProvider::VertexAi,
|
||||
BetaProvider::Databricks,
|
||||
],
|
||||
Self::MidConversationToolChanges20260701 => &[
|
||||
Self::InterleavedThinking20250514
|
||||
| Self::Output128k20250219
|
||||
| Self::TokenEfficientTools20250219
|
||||
| Self::WebSearch20250305 => &[BetaProvider::BedrockConverse, BetaProvider::Bedrock],
|
||||
Self::MidConversationOutputConfig20260701
|
||||
| Self::MidConversationToolChanges20260701 => &[
|
||||
BetaProvider::BedrockConverse,
|
||||
BetaProvider::BedrockMantle,
|
||||
BetaProvider::Databricks,
|
||||
],
|
||||
Self::Output128k20250219 | Self::TokenEfficientTools20250219 => &[
|
||||
BetaProvider::AzureAi,
|
||||
BetaProvider::BedrockConverse,
|
||||
BetaProvider::Bedrock,
|
||||
BetaProvider::VertexAi,
|
||||
],
|
||||
Self::PerTurnControl20260701 => &[
|
||||
BetaProvider::BedrockConverse,
|
||||
BetaProvider::Bedrock,
|
||||
BetaProvider::Databricks,
|
||||
],
|
||||
Self::StructuredOutputs20251113 => &[BetaProvider::Bedrock, BetaProvider::VertexAi],
|
||||
Self::ToolSearchTool20251019 => &[
|
||||
BetaProvider::Anthropic,
|
||||
BetaProvider::AzureAi,
|
||||
BetaProvider::BedrockConverse,
|
||||
BetaProvider::Databricks,
|
||||
],
|
||||
Self::InlineTools20260915 => &[
|
||||
BetaProvider::AzureAi,
|
||||
BetaProvider::BedrockConverse,
|
||||
BetaProvider::Bedrock,
|
||||
BetaProvider::BedrockMantle,
|
||||
BetaProvider::Databricks,
|
||||
],
|
||||
Self::StructuredOutputs20251113 => &[BetaProvider::Bedrock],
|
||||
Self::Other(_) => BetaProvider::VARIANTS,
|
||||
}
|
||||
}
|
||||
|
|
@ -451,7 +562,44 @@ mod tests {
|
|||
AnthropicBeta::TokenEfficientTools20250219,
|
||||
AnthropicBeta::ToolExamples20251029,
|
||||
AnthropicBeta::ToolSearchTool20251019,
|
||||
AnthropicBeta::InlineTools20260915
|
||||
AnthropicBeta::InlineTools20260915,
|
||||
AnthropicBeta::AfkMode20260131,
|
||||
AnthropicBeta::AgentMemory20260722,
|
||||
AnthropicBeta::AnalyticsUsageCost20260824,
|
||||
AnthropicBeta::CacheDiagnosis20260407,
|
||||
AnthropicBeta::CePlugins20260901,
|
||||
AnthropicBeta::CeUserManagement20260713,
|
||||
AnthropicBeta::CodeExecution20250522,
|
||||
AnthropicBeta::ComputerUse20241022,
|
||||
AnthropicBeta::DevFullThinking20250514,
|
||||
AnthropicBeta::Dreaming20260421,
|
||||
AnthropicBeta::ExperimentalClaudeTag20261021,
|
||||
AnthropicBeta::ExtendedCacheTtl20250411,
|
||||
AnthropicBeta::FallbackCredit20260601,
|
||||
AnthropicBeta::FallbackCredit20260701,
|
||||
AnthropicBeta::ManagedAgents20260401,
|
||||
AnthropicBeta::McpClient20260915,
|
||||
AnthropicBeta::McpTunnels20260622,
|
||||
AnthropicBeta::MessageBatches20240924,
|
||||
AnthropicBeta::MessageThreads20260812,
|
||||
AnthropicBeta::MidConversationSystemClearAt20260821,
|
||||
AnthropicBeta::ModelContextWindowExceeded20250826,
|
||||
AnthropicBeta::Output300k20260324,
|
||||
AnthropicBeta::Pdfs20240925,
|
||||
AnthropicBeta::PromptCaching20240731,
|
||||
AnthropicBeta::RedactThinking20260212,
|
||||
AnthropicBeta::ServerSideFallback20260601,
|
||||
AnthropicBeta::ServerSideFallback20260701,
|
||||
AnthropicBeta::SpendLimitReads20260926,
|
||||
AnthropicBeta::StructuredOutputs20251215,
|
||||
AnthropicBeta::TaskBudgets20260313,
|
||||
AnthropicBeta::TelemetryDestinations20260811,
|
||||
AnthropicBeta::ThinkingTokenCount20260513,
|
||||
AnthropicBeta::TokenCounting20241101,
|
||||
AnthropicBeta::UserProfiles20260324,
|
||||
AnthropicBeta::UserProfiles20260818,
|
||||
AnthropicBeta::UserProfiles20260904,
|
||||
AnthropicBeta::Webhooks20261007
|
||||
)]
|
||||
beta: AnthropicBeta,
|
||||
) {
|
||||
|
|
@ -551,7 +699,7 @@ mod tests {
|
|||
BetaProvider::Anthropic,
|
||||
Some(AnthropicBeta::Oauth20250420)
|
||||
)]
|
||||
#[case::rejected("effort-2025-11-24", BetaProvider::VertexAi, None)]
|
||||
#[case::rejected("skills-2025-10-02", BetaProvider::VertexAi, None)]
|
||||
#[case::unknown("example-beta-2099-01-01", BetaProvider::Anthropic, None)]
|
||||
fn on_resolves_known_spellings_held_as_other(
|
||||
#[case] raw: &str,
|
||||
|
|
@ -580,12 +728,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn unknown_values_are_kept_verbatim() {
|
||||
let parsed: AnthropicBeta = "claude-code-20250219".parse().unwrap();
|
||||
assert_eq!(
|
||||
parsed,
|
||||
AnthropicBeta::Other("claude-code-20250219".to_string())
|
||||
let parsed: AnthropicBeta = "example-beta-2099-01-01".parse().unwrap();
|
||||
assert!(
|
||||
matches!(&parsed, AnthropicBeta::Other(value) if value == "example-beta-2099-01-01")
|
||||
);
|
||||
assert_eq!(parsed.to_string(), "claude-code-20250219");
|
||||
assert_eq!(parsed.to_string(), "example-beta-2099-01-01");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -96,7 +96,10 @@ mod tests {
|
|||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::azure_ai(BetaProvider::AzureAi, "fast-mode-2026-02-01,example-beta-2099-01-01")]
|
||||
#[case::azure_ai(
|
||||
BetaProvider::AzureAi,
|
||||
"advisor-tool-2026-03-01,example-beta-2099-01-01"
|
||||
)]
|
||||
#[case::anthropic(BetaProvider::Anthropic, "bash_20241022")]
|
||||
#[case::vertex_ai(BetaProvider::VertexAi, " , ")]
|
||||
fn filter_removes_header_when_nothing_survives(
|
||||
|
|
|
|||
|
|
@ -186,6 +186,9 @@ pub struct ModelInfoFields {
|
|||
pub supports_image_size: Option<bool>,
|
||||
pub supported_audio_formats: Option<Value>,
|
||||
pub token_count_framing: Option<i64>,
|
||||
pub token_count_tool_framing: Option<i64>,
|
||||
pub token_count_responses_framing: Option<i64>,
|
||||
pub token_count_responses_tool_framing: Option<i64>,
|
||||
pub token_count_model: Option<Value>,
|
||||
pub token_count_tokenizer: Option<Value>,
|
||||
pub vertex_ai_audio_api: Option<Value>,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use crate::error::{DateCause, Error, Repr};
|
|||
use crate::vocabulary::{
|
||||
AudioFormat, EffortLevel, Endpoint, InputModality, Mode, OutputModality, VertexAiAudioApi,
|
||||
};
|
||||
use crate::wire::lax::Count;
|
||||
use crate::wire::lax::{Count, NumericCount};
|
||||
|
||||
macro_rules! attributes {
|
||||
($($(#[$doc:meta])* $name:ident: $ty:ty $(as $via:ty)?),* $(,)?) => {
|
||||
|
|
@ -104,9 +104,12 @@ attributes! {
|
|||
/// Cloud regions the model is available in ('global' or region ids).
|
||||
supported_regions: Vec<String>,
|
||||
thinking_always_on: bool,
|
||||
token_count_framing: u64 as Count,
|
||||
token_count_framing: u64 as NumericCount,
|
||||
token_count_model: TokenCountModel,
|
||||
token_count_responses_framing: u64 as NumericCount,
|
||||
token_count_responses_tool_framing: u64 as NumericCount,
|
||||
token_count_tokenizer: TokenCountTokenizer,
|
||||
token_count_tool_framing: u64 as NumericCount,
|
||||
/// Provider default tokens-per-minute limit.
|
||||
tpm: u64 as Count,
|
||||
use_openai_responses_path: bool,
|
||||
|
|
@ -302,6 +305,18 @@ mod tests {
|
|||
#[case::reasoning_modes("reasoning_modes", json!(["none", "adaptive"]), true)]
|
||||
#[case::reasoning_context_not_a_list("reasoning_contexts", json!("reasoning"), false)]
|
||||
#[case::reasoning_mode_not_a_string("reasoning_modes", json!([1]), false)]
|
||||
#[case::framing("token_count_framing", json!(12), true)]
|
||||
#[case::framing_written_as_text("token_count_framing", json!("12"), false)]
|
||||
#[case::framing_written_as_whole_float("token_count_framing", json!(12.0), true)]
|
||||
#[case::fractional_tool_framing("token_count_tool_framing", json!(1.5), false)]
|
||||
#[case::tool_framing("token_count_tool_framing", json!(214), true)]
|
||||
#[case::tool_framing_written_as_text("token_count_tool_framing", json!("214"), false)]
|
||||
#[case::tool_framing_as_a_flag("token_count_tool_framing", json!(true), false)]
|
||||
#[case::negative_tool_framing("token_count_tool_framing", json!(-1), false)]
|
||||
#[case::responses_framing("token_count_responses_framing", json!(38), true)]
|
||||
#[case::responses_framing_as_a_flag("token_count_responses_framing", json!(false), false)]
|
||||
#[case::responses_tool_framing("token_count_responses_tool_framing", json!(267), true)]
|
||||
#[case::responses_tool_framing_written_as_text("token_count_responses_tool_framing", json!("267"), false)]
|
||||
fn each_attribute_reads_its_own_type(
|
||||
#[case] key: &str,
|
||||
#[case] value: Value,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ pub(crate) struct LaxI64;
|
|||
|
||||
pub(crate) struct Count;
|
||||
|
||||
pub(crate) struct NumericCount;
|
||||
|
||||
impl<'de> DeserializeAs<'de, i64> for LaxI64 {
|
||||
fn deserialize_as<D: Deserializer<'de>>(deserializer: D) -> Result<i64, D::Error> {
|
||||
deserializer.deserialize_any(Self)
|
||||
|
|
@ -52,6 +54,34 @@ impl<'de> DeserializeAs<'de, u64> for Count {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeAs<'de, u64> for NumericCount {
|
||||
fn deserialize_as<D: Deserializer<'de>>(deserializer: D) -> Result<u64, D::Error> {
|
||||
deserializer.deserialize_any(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Visitor<'_> for NumericCount {
|
||||
type Value = u64;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("a whole number of zero or more")
|
||||
}
|
||||
|
||||
fn visit_u64<E: de::Error>(self, value: u64) -> Result<u64, E> {
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn visit_i64<E: de::Error>(self, value: i64) -> Result<u64, E> {
|
||||
u64::try_from(value).map_err(|_| E::invalid_value(de::Unexpected::Signed(value), &self))
|
||||
}
|
||||
|
||||
fn visit_f64<E: de::Error>(self, value: f64) -> Result<u64, E> {
|
||||
integral_float(value)
|
||||
.and_then(|whole| u64::try_from(whole).ok())
|
||||
.ok_or_else(|| E::invalid_value(de::Unexpected::Float(value), &self))
|
||||
}
|
||||
}
|
||||
|
||||
fn integer_string(value: &str) -> Option<i64> {
|
||||
let integer = match value.split_once('.') {
|
||||
Some((integer, fraction)) => {
|
||||
|
|
@ -99,6 +129,12 @@ mod tests {
|
|||
.map(DeserializeAsWrap::into_inner)
|
||||
}
|
||||
|
||||
fn numeric_count(value: Value) -> Option<u64> {
|
||||
DeserializeAsWrap::<u64, NumericCount>::deserialize(value)
|
||||
.ok()
|
||||
.map(DeserializeAsWrap::into_inner)
|
||||
}
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[rstest]
|
||||
|
|
@ -125,4 +161,23 @@ mod tests {
|
|||
) {
|
||||
assert_eq!(count(written), expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::integer(json!(214), Some(214))]
|
||||
#[case::zero(json!(0), Some(0))]
|
||||
#[case::whole_float(json!(214.0), Some(214))]
|
||||
#[case::negative_zero(json!(-0.0), Some(0))]
|
||||
#[case::above_i64(json!(u64::MAX), Some(u64::MAX))]
|
||||
#[case::negative(json!(-1), None)]
|
||||
#[case::negative_whole_float(json!(-1.0), None)]
|
||||
#[case::fraction(json!(1.5), None)]
|
||||
#[case::string(json!("214"), None)]
|
||||
#[case::boolean(json!(true), None)]
|
||||
#[case::list(json!([214]), None)]
|
||||
fn numeric_counts_accept_only_whole_json_numbers(
|
||||
#[case] written: Value,
|
||||
#[case] expected: Option<u64>,
|
||||
) {
|
||||
assert_eq!(numeric_count(written), expected);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1647,6 +1647,7 @@ def set_global_gitlab_config(config: Mapping[str, object]) -> None:
|
|||
# Lazy loading system for heavy modules to reduce initial import time and memory usage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import guardrails as guardrails
|
||||
from litellm.types.utils import ModelInfo as _ModelInfoType
|
||||
from litellm.types.utils import PriorityReservationSettings
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
|
||||
|
|
@ -2499,10 +2500,10 @@ def __getattr__(name: str) -> Any:
|
|||
handler_func: Final = registry[name]
|
||||
return handler_func(name)
|
||||
|
||||
if name == "proxy":
|
||||
if name in ("proxy", "guardrails"):
|
||||
import importlib
|
||||
|
||||
return importlib.import_module("litellm.proxy")
|
||||
return importlib.import_module(f"litellm.{name}")
|
||||
|
||||
if name == "harness" or name in _AGENT_EXPORTS:
|
||||
import importlib
|
||||
|
|
|
|||
|
|
@ -37,7 +37,47 @@
|
|||
"mid-conversation-output-config-2026-07-01": "mid-conversation-output-config-2026-07-01",
|
||||
"thinking-display-updates-2026-08-18": "thinking-display-updates-2026-08-18",
|
||||
"mid-conversation-tool-changes-2026-07-01": "mid-conversation-tool-changes-2026-07-01",
|
||||
"inline-tools-2026-09-15": "inline-tools-2026-09-15"
|
||||
"inline-tools-2026-09-15": "inline-tools-2026-09-15",
|
||||
"agent-memory-2026-07-22": null,
|
||||
"analytics-usage-cost-2026-08-24": "analytics-usage-cost-2026-08-24",
|
||||
"cache-diagnosis-2026-04-07": "cache-diagnosis-2026-04-07",
|
||||
"ce-plugins-2026-09-01": "ce-plugins-2026-09-01",
|
||||
"ce-user-management-2026-07-13": "ce-user-management-2026-07-13",
|
||||
"claude-code-20250219": "claude-code-20250219",
|
||||
"code-execution-2025-05-22": "code-execution-2025-05-22",
|
||||
"computer-use-2024-10-22": "computer-use-2024-10-22",
|
||||
"dev-full-thinking-2025-05-14": null,
|
||||
"dreaming-2026-04-21": "dreaming-2026-04-21",
|
||||
"experimental-claude-tag-2026-10-21": "experimental-claude-tag-2026-10-21",
|
||||
"extended-cache-ttl-2025-04-11": "extended-cache-ttl-2025-04-11",
|
||||
"fallback-credit-2026-06-01": "fallback-credit-2026-06-01",
|
||||
"fallback-credit-2026-07-01": "fallback-credit-2026-07-01",
|
||||
"managed-agents-2026-04-01": "managed-agents-2026-04-01",
|
||||
"mcp-client-2026-09-15": "mcp-client-2026-09-15",
|
||||
"mcp-tunnels-2026-06-22": "mcp-tunnels-2026-06-22",
|
||||
"message-batches-2024-09-24": "message-batches-2024-09-24",
|
||||
"message-threads-2026-08-12": "message-threads-2026-08-12",
|
||||
"mid-conversation-system-clear-at-2026-08-21": "mid-conversation-system-clear-at-2026-08-21",
|
||||
"model-context-window-exceeded-2025-08-26": "model-context-window-exceeded-2025-08-26",
|
||||
"output-300k-2026-03-24": "output-300k-2026-03-24",
|
||||
"pdfs-2024-09-25": "pdfs-2024-09-25",
|
||||
"prompt-caching-2024-07-31": "prompt-caching-2024-07-31",
|
||||
"server-side-fallback-2026-06-01": "server-side-fallback-2026-06-01",
|
||||
"server-side-fallback-2026-07-01": "server-side-fallback-2026-07-01",
|
||||
"spend-limit-reads-2026-09-26": "spend-limit-reads-2026-09-26",
|
||||
"task-budgets-2026-03-13": "task-budgets-2026-03-13",
|
||||
"telemetry-destinations-2026-08-11": "telemetry-destinations-2026-08-11",
|
||||
"thinking-token-count-2026-05-13": "thinking-token-count-2026-05-13",
|
||||
"token-counting-2024-11-01": "token-counting-2024-11-01",
|
||||
"tool-examples-2025-10-29": "tool-examples-2025-10-29",
|
||||
"tool-search-tool-2025-10-19": "tool-search-tool-2025-10-19",
|
||||
"user-profiles-2026-03-24": "user-profiles-2026-03-24",
|
||||
"user-profiles-2026-08-18": "user-profiles-2026-08-18",
|
||||
"user-profiles-2026-09-04": "user-profiles-2026-09-04",
|
||||
"webhooks-2026-10-07": "webhooks-2026-10-07",
|
||||
"afk-mode-2026-01-31": "afk-mode-2026-01-31",
|
||||
"redact-thinking-2026-02-12": "redact-thinking-2026-02-12",
|
||||
"structured-outputs-2025-12-15": "structured-outputs-2025-12-15"
|
||||
},
|
||||
"azure_ai": {
|
||||
"advisor-tool-2026-03-01": null,
|
||||
|
|
@ -45,23 +85,21 @@
|
|||
"bash_20241022": null,
|
||||
"bash_20250124": null,
|
||||
"code-execution-2025-08-25": "code-execution-2025-08-25",
|
||||
"compact-2026-01-12": null,
|
||||
"compact-2026-09-04": "compact-2026-09-04",
|
||||
"compact-2026-01-12": "compact-2026-01-12",
|
||||
"computer-use-2025-01-24": "computer-use-2025-01-24",
|
||||
"computer-use-2025-11-24": "computer-use-2025-11-24",
|
||||
"context-1m-2025-08-07": "context-1m-2025-08-07",
|
||||
"context-management-2025-06-27": "context-management-2025-06-27",
|
||||
"dangerous-tool-use-2026-09-03": "dangerous-tool-use-2026-09-03",
|
||||
"effort-2025-11-24": "effort-2025-11-24",
|
||||
"fast-mode-2026-02-01": null,
|
||||
"fast-mode-2026-02-01": "fast-mode-2026-02-01",
|
||||
"files-api-2025-04-14": "files-api-2025-04-14",
|
||||
"fine-grained-tool-streaming-2025-05-14": null,
|
||||
"fine-grained-tool-streaming-2025-05-14": "fine-grained-tool-streaming-2025-05-14",
|
||||
"interleaved-thinking-2025-05-14": "interleaved-thinking-2025-05-14",
|
||||
"mcp-client-2025-11-20": "mcp-client-2025-11-20",
|
||||
"mcp-client-2025-04-04": "mcp-client-2025-04-04",
|
||||
"mcp-servers-2025-12-04": null,
|
||||
"mid-conversation-tool-changes-2026-07-01": "mid-conversation-tool-changes-2026-07-01",
|
||||
"output-128k-2025-02-19": null,
|
||||
"output-128k-2025-02-19": "output-128k-2025-02-19",
|
||||
"structured-output-2024-03-01": null,
|
||||
"per-turn-control-2026-07-01": "per-turn-control-2026-07-01",
|
||||
"prompt-caching-scope-2026-01-05": "prompt-caching-scope-2026-01-05",
|
||||
|
|
@ -71,9 +109,54 @@
|
|||
"text_editor_20250124": null,
|
||||
"thinking-binding-controls-2026-08-01": "thinking-binding-controls-2026-08-01",
|
||||
"thinking-display-updates-2026-08-18": "thinking-display-updates-2026-08-18",
|
||||
"token-efficient-tools-2025-02-19": null,
|
||||
"token-efficient-tools-2025-02-19": "token-efficient-tools-2025-02-19",
|
||||
"web-fetch-2025-09-10": "web-fetch-2025-09-10",
|
||||
"web-search-2025-03-05": "web-search-2025-03-05"
|
||||
"web-search-2025-03-05": "web-search-2025-03-05",
|
||||
"agent-memory-2026-07-22": null,
|
||||
"analytics-usage-cost-2026-08-24": "analytics-usage-cost-2026-08-24",
|
||||
"cache-diagnosis-2026-04-07": "cache-diagnosis-2026-04-07",
|
||||
"ce-plugins-2026-09-01": "ce-plugins-2026-09-01",
|
||||
"ce-user-management-2026-07-13": "ce-user-management-2026-07-13",
|
||||
"claude-code-20250219": "claude-code-20250219",
|
||||
"code-execution-2025-05-22": "code-execution-2025-05-22",
|
||||
"compact-2026-09-04": "compact-2026-09-04",
|
||||
"computer-use-2024-10-22": "computer-use-2024-10-22",
|
||||
"dev-full-thinking-2025-05-14": null,
|
||||
"dreaming-2026-04-21": "dreaming-2026-04-21",
|
||||
"experimental-claude-tag-2026-10-21": "experimental-claude-tag-2026-10-21",
|
||||
"extended-cache-ttl-2025-04-11": "extended-cache-ttl-2025-04-11",
|
||||
"fallback-credit-2026-06-01": "fallback-credit-2026-06-01",
|
||||
"fallback-credit-2026-07-01": "fallback-credit-2026-07-01",
|
||||
"inline-tools-2026-09-15": "inline-tools-2026-09-15",
|
||||
"managed-agents-2026-04-01": "managed-agents-2026-04-01",
|
||||
"mcp-client-2026-09-15": "mcp-client-2026-09-15",
|
||||
"mcp-tunnels-2026-06-22": "mcp-tunnels-2026-06-22",
|
||||
"message-batches-2024-09-24": "message-batches-2024-09-24",
|
||||
"message-threads-2026-08-12": "message-threads-2026-08-12",
|
||||
"mid-conversation-output-config-2026-07-01": "mid-conversation-output-config-2026-07-01",
|
||||
"mid-conversation-system-clear-at-2026-08-21": "mid-conversation-system-clear-at-2026-08-21",
|
||||
"mid-conversation-tool-changes-2026-07-01": "mid-conversation-tool-changes-2026-07-01",
|
||||
"model-context-window-exceeded-2025-08-26": "model-context-window-exceeded-2025-08-26",
|
||||
"oauth-2025-04-20": "oauth-2025-04-20",
|
||||
"output-300k-2026-03-24": "output-300k-2026-03-24",
|
||||
"pdfs-2024-09-25": "pdfs-2024-09-25",
|
||||
"prompt-caching-2024-07-31": "prompt-caching-2024-07-31",
|
||||
"server-side-fallback-2026-06-01": "server-side-fallback-2026-06-01",
|
||||
"server-side-fallback-2026-07-01": "server-side-fallback-2026-07-01",
|
||||
"spend-limit-reads-2026-09-26": "spend-limit-reads-2026-09-26",
|
||||
"task-budgets-2026-03-13": "task-budgets-2026-03-13",
|
||||
"telemetry-destinations-2026-08-11": "telemetry-destinations-2026-08-11",
|
||||
"thinking-token-count-2026-05-13": "thinking-token-count-2026-05-13",
|
||||
"token-counting-2024-11-01": "token-counting-2024-11-01",
|
||||
"tool-examples-2025-10-29": "tool-examples-2025-10-29",
|
||||
"tool-search-tool-2025-10-19": "tool-search-tool-2025-10-19",
|
||||
"user-profiles-2026-03-24": "user-profiles-2026-03-24",
|
||||
"user-profiles-2026-08-18": "user-profiles-2026-08-18",
|
||||
"user-profiles-2026-09-04": "user-profiles-2026-09-04",
|
||||
"webhooks-2026-10-07": "webhooks-2026-10-07",
|
||||
"afk-mode-2026-01-31": "afk-mode-2026-01-31",
|
||||
"redact-thinking-2026-02-12": "redact-thinking-2026-02-12",
|
||||
"structured-outputs-2025-12-15": "structured-outputs-2025-12-15"
|
||||
},
|
||||
"bedrock_converse": {
|
||||
"advisor-tool-2026-03-01": null,
|
||||
|
|
@ -196,34 +279,92 @@
|
|||
"context-1m-2025-08-07": "context-1m-2025-08-07",
|
||||
"context-management-2025-06-27": "context-management-2025-06-27",
|
||||
"dangerous-tool-use-2026-09-03": "dangerous-tool-use-2026-09-03",
|
||||
"effort-2025-11-24": null,
|
||||
"effort-2025-11-24": "effort-2025-11-24",
|
||||
"fast-mode-2026-02-01": null,
|
||||
"files-api-2025-04-14": null,
|
||||
"fine-grained-tool-streaming-2025-05-14": null,
|
||||
"fine-grained-tool-streaming-2025-05-14": "fine-grained-tool-streaming-2025-05-14",
|
||||
"interleaved-thinking-2025-05-14": "interleaved-thinking-2025-05-14",
|
||||
"mcp-client-2025-11-20": null,
|
||||
"mcp-client-2025-04-04": null,
|
||||
"mcp-servers-2025-12-04": null,
|
||||
"mid-conversation-tool-changes-2026-07-01": "mid-conversation-tool-changes-2026-07-01",
|
||||
"output-128k-2025-02-19": null,
|
||||
"output-128k-2025-02-19": "output-128k-2025-02-19",
|
||||
"structured-output-2024-03-01": null,
|
||||
"per-turn-control-2026-07-01": "per-turn-control-2026-07-01",
|
||||
"prompt-caching-scope-2026-01-05": null,
|
||||
"skills-2025-10-02": null,
|
||||
"structured-outputs-2025-11-13": null,
|
||||
"structured-outputs-2025-11-13": "structured-outputs-2025-11-13",
|
||||
"text_editor_20241022": null,
|
||||
"text_editor_20250124": null,
|
||||
"thinking-binding-controls-2026-08-01": "thinking-binding-controls-2026-08-01",
|
||||
"thinking-display-updates-2026-08-18": "thinking-display-updates-2026-08-18",
|
||||
"token-efficient-tools-2025-02-19": null,
|
||||
"token-efficient-tools-2025-02-19": "token-efficient-tools-2025-02-19",
|
||||
"tool-search-tool-2025-10-19": "tool-search-tool-2025-10-19",
|
||||
"web-fetch-2025-09-10": null,
|
||||
"web-search-2025-03-05": "web-search-2025-03-05",
|
||||
"inline-tools-2026-09-15": "inline-tools-2026-09-15"
|
||||
"inline-tools-2026-09-15": "inline-tools-2026-09-15",
|
||||
"agent-memory-2026-07-22": null,
|
||||
"analytics-usage-cost-2026-08-24": null,
|
||||
"cache-diagnosis-2026-04-07": null,
|
||||
"ce-plugins-2026-09-01": null,
|
||||
"ce-user-management-2026-07-13": null,
|
||||
"claude-code-20250219": "claude-code-20250219",
|
||||
"code-execution-2025-05-22": null,
|
||||
"compact-2026-09-04": null,
|
||||
"computer-use-2024-10-22": null,
|
||||
"dev-full-thinking-2025-05-14": null,
|
||||
"dreaming-2026-04-21": null,
|
||||
"experimental-claude-tag-2026-10-21": null,
|
||||
"extended-cache-ttl-2025-04-11": null,
|
||||
"fallback-credit-2026-06-01": "fallback-credit-2026-06-01",
|
||||
"fallback-credit-2026-07-01": "fallback-credit-2026-07-01",
|
||||
"managed-agents-2026-04-01": null,
|
||||
"mcp-client-2026-09-15": null,
|
||||
"mcp-tunnels-2026-06-22": null,
|
||||
"message-batches-2024-09-24": null,
|
||||
"message-threads-2026-08-12": null,
|
||||
"mid-conversation-output-config-2026-07-01": "mid-conversation-output-config-2026-07-01",
|
||||
"mid-conversation-system-clear-at-2026-08-21": "mid-conversation-system-clear-at-2026-08-21",
|
||||
"mid-conversation-tool-changes-2026-07-01": "mid-conversation-tool-changes-2026-07-01",
|
||||
"model-context-window-exceeded-2025-08-26": "model-context-window-exceeded-2025-08-26",
|
||||
"oauth-2025-04-20": null,
|
||||
"output-300k-2026-03-24": null,
|
||||
"pdfs-2024-09-25": "pdfs-2024-09-25",
|
||||
"prompt-caching-2024-07-31": null,
|
||||
"server-side-fallback-2026-06-01": "server-side-fallback-2026-06-01",
|
||||
"server-side-fallback-2026-07-01": null,
|
||||
"spend-limit-reads-2026-09-26": null,
|
||||
"task-budgets-2026-03-13": "task-budgets-2026-03-13",
|
||||
"telemetry-destinations-2026-08-11": null,
|
||||
"thinking-token-count-2026-05-13": "thinking-token-count-2026-05-13",
|
||||
"token-counting-2024-11-01": "token-counting-2024-11-01",
|
||||
"tool-examples-2025-10-29": "tool-examples-2025-10-29",
|
||||
"user-profiles-2026-03-24": null,
|
||||
"user-profiles-2026-08-18": null,
|
||||
"user-profiles-2026-09-04": null,
|
||||
"webhooks-2026-10-07": null,
|
||||
"afk-mode-2026-01-31": "afk-mode-2026-01-31",
|
||||
"redact-thinking-2026-02-12": null,
|
||||
"structured-outputs-2025-12-15": "structured-outputs-2025-12-15"
|
||||
},
|
||||
"vertex_ai_header_only": {
|
||||
"ce-plugins-2026-09-01": "ce-plugins-2026-09-01",
|
||||
"ce-user-management-2026-07-13": "ce-user-management-2026-07-13",
|
||||
"compact-2026-09-04": "compact-2026-09-04",
|
||||
"inline-tools-2026-09-15": "inline-tools-2026-09-15"
|
||||
"dreaming-2026-04-21": "dreaming-2026-04-21",
|
||||
"experimental-claude-tag-2026-10-21": "experimental-claude-tag-2026-10-21",
|
||||
"extended-cache-ttl-2025-04-11": "extended-cache-ttl-2025-04-11",
|
||||
"inline-tools-2026-09-15": "inline-tools-2026-09-15",
|
||||
"managed-agents-2026-04-01": "managed-agents-2026-04-01",
|
||||
"mcp-tunnels-2026-06-22": "mcp-tunnels-2026-06-22",
|
||||
"oauth-2025-04-20": "oauth-2025-04-20",
|
||||
"prompt-caching-2024-07-31": "prompt-caching-2024-07-31",
|
||||
"prompt-caching-scope-2026-01-05": "prompt-caching-scope-2026-01-05",
|
||||
"redact-thinking-2026-02-12": "redact-thinking-2026-02-12",
|
||||
"spend-limit-reads-2026-09-26": "spend-limit-reads-2026-09-26",
|
||||
"telemetry-destinations-2026-08-11": "telemetry-destinations-2026-08-11",
|
||||
"user-profiles-2026-03-24": "user-profiles-2026-03-24",
|
||||
"user-profiles-2026-08-18": "user-profiles-2026-08-18",
|
||||
"user-profiles-2026-09-04": "user-profiles-2026-09-04"
|
||||
},
|
||||
"databricks": {
|
||||
"advisor-tool-2026-03-01": null,
|
||||
|
|
|
|||
|
|
@ -45,13 +45,27 @@ from .redis_semantic_cache import RedisSemanticCache
|
|||
from .s3_cache import S3Cache
|
||||
|
||||
RESPONSE_CACHE_TARGET: Final = "llm_response"
|
||||
_ALWAYS_KEYED_PROVIDER_PARAMS: Final = frozenset({"continuation_token"})
|
||||
_ALWAYS_KEYED_PROVIDER_PARAMS: Final = frozenset({"continuation_token", "return_token_ids"})
|
||||
_DECODED_LITERAL: Final = TypeAdapter(object)
|
||||
_REDIS_STARTUP_NODES_ADAPTER: Final = TypeAdapter(
|
||||
list[dict[str, object]], config=ConfigDict(strict=True, hide_input_in_errors=True)
|
||||
)
|
||||
|
||||
|
||||
def _keyed_provider_params_in_extra_body(kwargs: Mapping[str, object]) -> str:
|
||||
raw_extra_body: Final = kwargs.get("extra_body")
|
||||
if not isinstance(raw_extra_body, dict):
|
||||
return ""
|
||||
extra_body: Final = cast( # cast-ok: isinstance leaves the key and value types unknown
|
||||
Mapping[str, object], raw_extra_body
|
||||
)
|
||||
return "".join(
|
||||
f"extra_body.{param}: {extra_body[param]}"
|
||||
for param in sorted(_ALWAYS_KEYED_PROVIDER_PARAMS)
|
||||
if extra_body.get(param)
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def response_cache_phase(operation: Literal["get", "set"]) -> Generator[None]:
|
||||
"""The ``cache.get llm_response`` / ``cache.set llm_response`` span a response-cache read or write runs
|
||||
|
|
@ -429,7 +443,10 @@ class Cache:
|
|||
"" if embeds_thought_signature_in_tool_call_id(kwargs) else "thought_signature_in_tool_call_id: False"
|
||||
)
|
||||
hashed_cache_key = Cache._get_hashed_cache_key(
|
||||
cache_key + reasoning_summary_material + tool_call_id_shape_material
|
||||
cache_key
|
||||
+ _keyed_provider_params_in_extra_body(kwargs)
|
||||
+ reasoning_summary_material
|
||||
+ tool_call_id_shape_material
|
||||
)
|
||||
hashed_cache_key = self._add_namespace_to_cache_key(hashed_cache_key, **kwargs)
|
||||
verbose_logger.debug(
|
||||
|
|
|
|||
|
|
@ -34,10 +34,12 @@ from litellm.completion_extras.litellm_responses_transformation.history import (
|
|||
requested_phase,
|
||||
)
|
||||
from litellm.integrations.anthropic_cache_control_hook import supports_openai_prompt_cache_breakpoint
|
||||
from litellm.litellm_core_utils.core_helpers import drops_unsupported_params
|
||||
from litellm.litellm_core_utils.hidden_params import get_hidden_params, get_or_create_hidden_params
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
prompt_cache_breakpoint_for_wire,
|
||||
prompt_cache_breakpoint_with_readable_mode,
|
||||
responses_reasoning_items_from_thinking_blocks,
|
||||
whole_prompt_cache_breakpoint_or_none,
|
||||
with_prompt_cache_breakpoint,
|
||||
)
|
||||
from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator
|
||||
|
|
@ -97,6 +99,12 @@ _RESPONSES_API_ONLY_FIELDS: Final = frozenset((*Response.model_fields, *Response
|
|||
)
|
||||
|
||||
|
||||
def _bridged_prompt_cache_breakpoint(marker: object, drop_params: bool, target_trims_breakpoints: bool) -> object:
|
||||
if target_trims_breakpoints:
|
||||
return prompt_cache_breakpoint_with_readable_mode(marker, drop_params)
|
||||
return whole_prompt_cache_breakpoint_or_none(marker) if drop_params else marker
|
||||
|
||||
|
||||
def _strip_prompt_cache_breakpoint_from_content_block(value: object) -> object:
|
||||
if not isinstance(value, dict):
|
||||
return value
|
||||
|
|
@ -844,6 +852,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
messages: list["AllMessageValues"],
|
||||
*,
|
||||
drop_params: bool = False,
|
||||
target_trims_breakpoints: bool = True,
|
||||
keep_prompt_cache_breakpoints: bool = False,
|
||||
replay_tool_search: bool = False,
|
||||
include_namespace: bool = False,
|
||||
|
|
@ -852,6 +861,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
converted_input_items, instructions = self._convert_chat_completion_messages_to_responses_input(
|
||||
messages,
|
||||
drop_params=drop_params,
|
||||
target_trims_breakpoints=target_trims_breakpoints,
|
||||
replay_tool_search=replay_tool_search,
|
||||
include_namespace=include_namespace,
|
||||
replay_openai_history=replay_openai_history,
|
||||
|
|
@ -868,6 +878,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
messages: list["AllMessageValues"],
|
||||
*,
|
||||
drop_params: bool = False,
|
||||
target_trims_breakpoints: bool = True,
|
||||
replay_tool_search: bool = False,
|
||||
include_namespace: bool = False,
|
||||
replay_openai_history: bool = False,
|
||||
|
|
@ -921,6 +932,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
content,
|
||||
role,
|
||||
drop_params=drop_params,
|
||||
target_trims_breakpoints=target_trims_breakpoints,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
|
@ -940,6 +952,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
content,
|
||||
"user", # Use "user" role to get input_* types
|
||||
drop_params=drop_params,
|
||||
target_trims_breakpoints=target_trims_breakpoints,
|
||||
)
|
||||
else:
|
||||
# Fallback: convert unexpected types to input_text
|
||||
|
|
@ -968,7 +981,10 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": self._convert_content_to_responses_format(
|
||||
content, "assistant", drop_params=drop_params
|
||||
content,
|
||||
"assistant",
|
||||
drop_params=drop_params,
|
||||
target_trims_breakpoints=target_trims_breakpoints,
|
||||
),
|
||||
**_assistant_phase_field(msg, replay_openai_history, drop_params),
|
||||
}
|
||||
|
|
@ -1009,7 +1025,10 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
"type": "message",
|
||||
"role": role,
|
||||
"content": self._convert_content_to_responses_format(
|
||||
content, cast(str, role), drop_params=drop_params
|
||||
content,
|
||||
cast(str, role),
|
||||
drop_params=drop_params,
|
||||
target_trims_breakpoints=target_trims_breakpoints,
|
||||
),
|
||||
**_assistant_phase_field(msg, replay_openai_history and role == "assistant", drop_params),
|
||||
}
|
||||
|
|
@ -1135,6 +1154,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
converted_input_items, converted_instructions = self.convert_chat_completion_messages_to_responses_api(
|
||||
messages,
|
||||
drop_params=bool(litellm_params.get("drop_params") or litellm.drop_params),
|
||||
target_trims_breakpoints=drops_unsupported_params(litellm_params.get("drop_params")),
|
||||
keep_prompt_cache_breakpoints=supports_prompt_cache_breakpoint,
|
||||
replay_tool_search=_requests_tool_search(cast(object, optional_params)), # cast-ok: upcast
|
||||
include_namespace=True,
|
||||
|
|
@ -1640,6 +1660,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
| None,
|
||||
role: str,
|
||||
drop_params: bool = False,
|
||||
target_trims_breakpoints: bool = True,
|
||||
) -> list[dict[str, object]]:
|
||||
"""Convert chat completion content to responses API format"""
|
||||
from litellm.types.llms.openai import ChatCompletionImageObject
|
||||
|
|
@ -1666,7 +1687,9 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
if original_type == "text":
|
||||
converted = with_prompt_cache_breakpoint(
|
||||
self._convert_content_str_to_input_text(item.get("text", ""), role),
|
||||
prompt_cache_breakpoint_for_wire(item.get("prompt_cache_breakpoint"), drop_params),
|
||||
_bridged_prompt_cache_breakpoint(
|
||||
item.get("prompt_cache_breakpoint"), drop_params, target_trims_breakpoints
|
||||
),
|
||||
)
|
||||
result.append(converted)
|
||||
verbose_logger.debug("Chat provider: text -> %s", converted)
|
||||
|
|
@ -1679,14 +1702,18 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
cast(ChatCompletionImageObject, item), role
|
||||
),
|
||||
),
|
||||
prompt_cache_breakpoint_for_wire(item.get("prompt_cache_breakpoint"), drop_params),
|
||||
_bridged_prompt_cache_breakpoint(
|
||||
item.get("prompt_cache_breakpoint"), drop_params, target_trims_breakpoints
|
||||
),
|
||||
)
|
||||
result.append(converted)
|
||||
verbose_logger.debug("Chat provider: image_url -> %s", converted)
|
||||
elif original_type == "input_audio":
|
||||
converted = with_prompt_cache_breakpoint(
|
||||
{"type": "input_audio", "input_audio": item.get("input_audio")},
|
||||
prompt_cache_breakpoint_for_wire(item.get("prompt_cache_breakpoint"), drop_params),
|
||||
_bridged_prompt_cache_breakpoint(
|
||||
item.get("prompt_cache_breakpoint"), drop_params, target_trims_breakpoints
|
||||
),
|
||||
)
|
||||
result.append(converted)
|
||||
verbose_logger.debug("Chat provider: input_audio -> %s", converted)
|
||||
|
|
@ -1702,7 +1729,9 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
_input_file_from_file_value(
|
||||
cast("ChatCompletionFileObject", item).get("file"), # cast-ok: type tag checked
|
||||
),
|
||||
prompt_cache_breakpoint_for_wire(item.get("prompt_cache_breakpoint"), drop_params),
|
||||
_bridged_prompt_cache_breakpoint(
|
||||
item.get("prompt_cache_breakpoint"), drop_params, target_trims_breakpoints
|
||||
),
|
||||
)
|
||||
result.append(converted)
|
||||
verbose_logger.debug("Chat provider: file -> %s", converted)
|
||||
|
|
@ -1726,7 +1755,9 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
# Default to input_text for unknown types
|
||||
converted = with_prompt_cache_breakpoint(
|
||||
self._convert_content_str_to_input_text(str(item.get("text", item)), role),
|
||||
prompt_cache_breakpoint_for_wire(item.get("prompt_cache_breakpoint"), drop_params),
|
||||
_bridged_prompt_cache_breakpoint(
|
||||
item.get("prompt_cache_breakpoint"), drop_params, target_trims_breakpoints
|
||||
),
|
||||
)
|
||||
result.append(converted)
|
||||
verbose_logger.debug("Chat provider: unknown(%s) -> %s", original_type, converted)
|
||||
|
|
|
|||
|
|
@ -699,6 +699,9 @@ STREAM_SSE_DONE_STRING: Final[str] = "[DONE]"
|
|||
STREAM_SSE_DATA_PREFIX: Final[str] = "data: "
|
||||
STREAM_SSE_KEEPALIVE_PING_CHUNK: Final[str] = 'event: ping\ndata: {"type": "ping"}\n\n'
|
||||
STREAM_SSE_KEEPALIVE_PING_BYTES: Final[bytes] = STREAM_SSE_KEEPALIVE_PING_CHUNK.encode("utf-8")
|
||||
STREAM_FINISH_CHOICE_FIELDS: Final = ("stop_reason",)
|
||||
STREAM_CHUNK_CHOICE_FIELDS: Final = ("token_ids",)
|
||||
VLLM_CHOICE_FIELDS: Final = STREAM_FINISH_CHOICE_FIELDS + STREAM_CHUNK_CHOICE_FIELDS
|
||||
### SPEND TRACKING ###
|
||||
DEFAULT_REPLICATE_GPU_PRICE_PER_SECOND: Final = get_env_float(
|
||||
"DEFAULT_REPLICATE_GPU_PRICE_PER_SECOND", 0.001400
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import time
|
|||
from collections.abc import Mapping, Sequence
|
||||
from datetime import datetime
|
||||
from functools import lru_cache
|
||||
from itertools import accumulate
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, TypeAlias, cast
|
||||
|
||||
|
|
@ -3886,44 +3887,65 @@ def handle_realtime_transcription_cost_calculation(
|
|||
- {"type": "duration", "seconds": <float>} → priced via input_cost_per_second
|
||||
- {"type": "tokens", "input_tokens": ...} → priced via input/audio token cost
|
||||
"""
|
||||
completed_events: Final = [
|
||||
cast(dict, result) for result in results if result.get("type") == _TRANSCRIPTION_COMPLETED_EVENT_TYPE
|
||||
]
|
||||
if not completed_events:
|
||||
configured_models: Final = tuple(accumulate(map(_session_transcription_model, results), _newer_model))
|
||||
clips: Final = tuple(
|
||||
(as_str_mapping(result.get("usage")) or {}, configured)
|
||||
for result, configured in zip(results, configured_models, strict=True)
|
||||
if result.get("type") == _TRANSCRIPTION_COMPLETED_EVENT_TYPE
|
||||
)
|
||||
if not clips:
|
||||
return 0.0
|
||||
|
||||
model_name: Final = _get_transcription_model_name_from_results(results) or litellm_model_name
|
||||
model_info: Final = _cost_map_model_info(model_name, custom_llm_provider)
|
||||
fallback_model: Final = _get_transcription_model_name_from_results(results) or litellm_model_name
|
||||
clip_models: Final = tuple((usage, configured or fallback_model) for usage, configured in clips)
|
||||
model_infos: Final = MappingProxyType(
|
||||
{model: _cost_map_model_info(model, custom_llm_provider) for model in frozenset(m for _, m in clip_models)}
|
||||
)
|
||||
override_info: Final = (
|
||||
_cost_map_model_info(custom_pricing_model, custom_llm_provider) if custom_pricing_model is not None else None
|
||||
)
|
||||
return sum(
|
||||
(_transcription_usage_cost(dict(usage), model_infos[model], override_info) for usage, model in clip_models),
|
||||
0.0,
|
||||
)
|
||||
|
||||
total_cost = 0.0
|
||||
for event in completed_events:
|
||||
usage = event.get("usage") or {}
|
||||
total_cost += _transcription_usage_cost(usage, model_info, override_info)
|
||||
return total_cost
|
||||
|
||||
_TRANSCRIPTION_SESSION_EVENT_TYPES: Final = frozenset(
|
||||
{"transcription_session.created", "transcription_session.updated", "session.created", "session.updated"}
|
||||
)
|
||||
|
||||
|
||||
def _str_field(mapping: Mapping[str, object] | None, key: str) -> str | None:
|
||||
value: Final = None if mapping is None else mapping.get(key)
|
||||
return value if isinstance(value, str) and value else None
|
||||
|
||||
|
||||
def _configured_transcription_model(session: Mapping[str, object]) -> str | None:
|
||||
audio_input: Final = as_str_mapping((as_str_mapping(session.get("audio")) or {}).get("input"))
|
||||
nested: Final = _str_field(as_str_mapping((audio_input or {}).get("transcription")), "model")
|
||||
return nested or _str_field(as_str_mapping(session.get("input_audio_transcription")), "model")
|
||||
|
||||
|
||||
def _session_of(result: Mapping[str, object]) -> Mapping[str, object] | None:
|
||||
return as_str_mapping(result.get("session")) if result.get("type") in _TRANSCRIPTION_SESSION_EVENT_TYPES else None
|
||||
|
||||
|
||||
def _session_transcription_model(result: Mapping[str, object]) -> str | None:
|
||||
session: Final = _session_of(result)
|
||||
return None if session is None else _configured_transcription_model(session)
|
||||
|
||||
|
||||
def _newer_model(current: str | None, newer: str | None) -> str | None:
|
||||
return newer or current
|
||||
|
||||
|
||||
def _get_transcription_model_name_from_results(
|
||||
results: OpenAIRealtimeStreamList,
|
||||
) -> str | None:
|
||||
"""Resolve the ASR model from a transcription_session.* / session.* event."""
|
||||
for result in results:
|
||||
if result.get("type") in (
|
||||
"transcription_session.created",
|
||||
"transcription_session.updated",
|
||||
"session.created",
|
||||
"session.updated",
|
||||
):
|
||||
session = cast(dict, result).get("session", {}) or {}
|
||||
transcription = ((session.get("audio", {}) or {}).get("input", {}) or {}).get(
|
||||
"transcription", {}
|
||||
) or session.get("input_audio_transcription", {})
|
||||
model = (transcription or {}).get("model") or session.get("model")
|
||||
if model:
|
||||
return model
|
||||
return None
|
||||
return next(
|
||||
(model for result in results if (model := _str_field(_session_of(result), "model"))),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
_TranscriptionRateKey: TypeAlias = Literal[
|
||||
|
|
|
|||
66
litellm/guardrails.py
Normal file
66
litellm/guardrails.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"""Invoke configured gateway guardrails without generating a completion."""
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from enum import Enum
|
||||
from typing import Final
|
||||
|
||||
import httpx
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
import litellm
|
||||
from litellm.llms.custom_httpx.http_handler import (
|
||||
get_async_httpx_client, # pyright: ignore[reportUnknownVariableType] # shared factory has untyped optional parameters
|
||||
)
|
||||
from litellm.types.guardrails import NativeGuardrailResponse
|
||||
|
||||
_NATIVE_RESPONSE: Final[TypeAdapter[NativeGuardrailResponse]] = TypeAdapter(NativeGuardrailResponse)
|
||||
|
||||
|
||||
class _InputType(Enum):
|
||||
REQUEST = "request"
|
||||
RESPONSE = "response"
|
||||
|
||||
|
||||
async def apply(
|
||||
guardrail_name: str,
|
||||
*,
|
||||
input: str | None = None,
|
||||
response: str | None = None,
|
||||
messages: Sequence[Mapping[str, object]] | None = None,
|
||||
metadata: Mapping[str, object] | None = None,
|
||||
api_base: str | None = None,
|
||||
api_key: str | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> NativeGuardrailResponse:
|
||||
"""Return a configured guardrail's native structured findings.
|
||||
|
||||
Supply exactly one of ``input`` or ``response``. Grounding guardrails can
|
||||
use ``messages`` as evidence. Connection settings default to
|
||||
``litellm.api_base`` and ``litellm.api_key``.
|
||||
"""
|
||||
if (input is None) == (response is None):
|
||||
raise ValueError("Supply exactly one of input or response.")
|
||||
resolved_base: Final = api_base if api_base is not None else litellm.api_base
|
||||
if not resolved_base:
|
||||
raise ValueError("Set api_base or litellm.api_base to your LiteLLM gateway URL.")
|
||||
resolved_key: Final = api_key if api_key is not None else litellm.api_key
|
||||
input_type: Final = _InputType.RESPONSE if response is not None else _InputType.REQUEST
|
||||
payload: Final = dict[str, object](
|
||||
guardrail_name=guardrail_name,
|
||||
text=response if response is not None else input,
|
||||
input_type=input_type.value,
|
||||
response_format="native",
|
||||
)
|
||||
if messages is not None:
|
||||
payload["messages"] = [dict(message) for message in messages]
|
||||
if metadata is not None:
|
||||
payload["metadata"] = dict(metadata)
|
||||
gateway_root: Final = resolved_base.rstrip("/").removesuffix("/v1")
|
||||
client: Final = get_async_httpx_client(llm_provider="litellm_guardrails")
|
||||
result: Final = await client.post( # pyright: ignore[reportUnknownMemberType] # shared handler has untyped optional parameters
|
||||
url=f"{gateway_root}/guardrails/apply_guardrail",
|
||||
json=payload,
|
||||
headers={"Authorization": f"Bearer {resolved_key}"} if resolved_key else {},
|
||||
timeout=timeout,
|
||||
)
|
||||
return _NATIVE_RESPONSE.validate_json(result.content)
|
||||
|
|
@ -17,7 +17,7 @@ from collections.abc import Callable, Container, Iterable, Iterator, Mapping, Se
|
|||
from typing import TYPE_CHECKING, Any, Final, TypeAlias, TypeVar, cast
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
from pydantic import BaseModel, ConfigDict, TypeAdapter, ValidationError
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
|
|
@ -32,6 +32,7 @@ from litellm.litellm_core_utils.prompt_templates.factory import (
|
|||
tool_call_ids_answered_by_client,
|
||||
tool_call_ids_answered_in,
|
||||
)
|
||||
from litellm.litellm_core_utils.prompt_templates.mid_conversation_system import string_field
|
||||
from litellm.llms.anthropic.common_utils import (
|
||||
is_claude_code_one_shot_subagent_request,
|
||||
supports_anthropic_cache_control,
|
||||
|
|
@ -72,7 +73,7 @@ MAX_CACHE_CONTROL_BLOCKS: Final = 4
|
|||
|
||||
CACHE_BREAKPOINT_KEYS: Final = ("cache_control", "prompt_cache_breakpoint")
|
||||
OPENAI_PROMPT_CACHE_BREAKPOINT_MIN_GPT_VERSION: Final = (5, 6)
|
||||
_GPT_VERSION_PATTERN: Final = re.compile(r"^gpt-(\d+)(?:\.(\d+))?")
|
||||
_GPT_VERSION_PATTERN: Final = re.compile(r"^gpt-(\d+)(?:\.(\d+)|-(0|[1-9]\d?)(?![\da-z]))?")
|
||||
_FINE_TUNED_BASE_PATTERN: Final = re.compile(r"^ft:([^:]+)")
|
||||
_O_SERIES_PATTERN: Final = re.compile(r"^o\d+(?:-|$)")
|
||||
OPENAI_PROMPT_CACHE_BREAKPOINT_BLOCK_TYPES: Final = frozenset(
|
||||
|
|
@ -128,7 +129,7 @@ def _message_mark_reaches_provider(fields: Mapping[str, object]) -> bool:
|
|||
return False
|
||||
return (
|
||||
not isinstance(fields.get("content"), list)
|
||||
or fields.get("role") in _ROLES_SENDING_MESSAGE_MARK_WITH_LIST_CONTENT
|
||||
or string_field(fields, "role") in _ROLES_SENDING_MESSAGE_MARK_WITH_LIST_CONTENT
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -334,6 +335,30 @@ def openai_prompt_cache_breakpoint_support(model: str, custom_llm_provider: str
|
|||
return _named_model_breakpoint_support(model)
|
||||
|
||||
|
||||
class _DeclaredBaseModels(BaseModel):
|
||||
model_config = ConfigDict(frozen=True, extra="ignore", from_attributes=True)
|
||||
|
||||
base_model: object = None
|
||||
model_info: object = None
|
||||
|
||||
def names(self) -> tuple[str, ...]:
|
||||
model_info: Final = _validated_object_mapping(self.model_info) or {}
|
||||
candidates: Final = (self.base_model, model_info.get("base_model"))
|
||||
return tuple(name for name in candidates if isinstance(name, str) and name)
|
||||
|
||||
|
||||
def deployment_prompt_cache_breakpoint_support(
|
||||
model: str, custom_llm_provider: str, litellm_params: object
|
||||
) -> bool | None:
|
||||
"""``openai_prompt_cache_breakpoint_support`` for a deployment, read from its declared base model first."""
|
||||
declared: Final = _DeclaredBaseModels.model_validate(litellm_params)
|
||||
supports: Final = (
|
||||
openai_prompt_cache_breakpoint_support(name, custom_llm_provider)
|
||||
for name in dict.fromkeys((*declared.names(), model))
|
||||
)
|
||||
return next((support for support in supports if support is not None), None)
|
||||
|
||||
|
||||
def _named_model_breakpoint_support(model: str) -> bool | None:
|
||||
bare_model: Final = model.rsplit("/", 1)[-1].lower()
|
||||
if _O_SERIES_PATTERN.match(bare_model) is not None:
|
||||
|
|
@ -341,7 +366,7 @@ def _named_model_breakpoint_support(model: str) -> bool | None:
|
|||
version_match: Final = _GPT_VERSION_PATTERN.match(bare_model)
|
||||
if version_match is None:
|
||||
return None
|
||||
version: Final = (int(version_match.group(1)), int(version_match.group(2) or 0))
|
||||
version: Final = (int(version_match.group(1)), int(version_match.group(2) or version_match.group(3) or 0))
|
||||
return version >= OPENAI_PROMPT_CACHE_BREAKPOINT_MIN_GPT_VERSION
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3036,7 +3036,7 @@ class PrometheusLogger(CustomLogger):
|
|||
|
||||
async def async_post_call_failure_hook(
|
||||
self,
|
||||
request_data: dict,
|
||||
request_data: dict[str, object],
|
||||
original_exception: Exception,
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
traceback_str: str | None = None,
|
||||
|
|
@ -3059,10 +3059,14 @@ class PrometheusLogger(CustomLogger):
|
|||
try:
|
||||
_tags: Final = StandardLoggingPayloadSetup.get_request_tags(
|
||||
litellm_params=request_data,
|
||||
proxy_server_request=request_data.get("proxy_server_request", {}),
|
||||
proxy_server_request=_logged_fields(request_data.get("proxy_server_request")),
|
||||
)
|
||||
_metadata: Final = _logged_fields(request_data.get("metadata"))
|
||||
model_id: Final = (
|
||||
_logged_fields(_metadata.get("model_info")).get("id")
|
||||
or _logged_fields(request_data.get("model_info")).get("id")
|
||||
or _logged_fields(request_data.get("standard_logging_object")).get("model_id")
|
||||
)
|
||||
_metadata: Final = request_data.get("metadata", {}) or {}
|
||||
model_id: Final = _metadata.get("model_info", {}).get("id") or request_data.get("model_info", {}).get("id")
|
||||
rate_limit_category, rate_limit_type = self._extract_rate_limit_labels(original_exception)
|
||||
api_provider: Final = self._extract_api_provider_from_request_data(
|
||||
request_data
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import copy
|
|||
import logging
|
||||
import math
|
||||
import re
|
||||
from collections.abc import Collection, Iterable, Iterator, Mapping
|
||||
from collections.abc import Collection, Iterable, Iterator, Mapping, Sequence
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, Protocol, TypeVar, cast, runtime_checkable
|
||||
|
||||
|
|
@ -104,6 +104,10 @@ def as_str_mapping(value: object) -> Mapping[str, object] | None:
|
|||
return value if isinstance(value, Mapping) else None
|
||||
|
||||
|
||||
def as_object_list(value: object) -> Sequence[object] | None:
|
||||
return value if isinstance(value, list) else None
|
||||
|
||||
|
||||
def decoded_json_object(value: object) -> Mapping[str, object]:
|
||||
mapping: Final = as_str_mapping(value)
|
||||
if mapping is not None:
|
||||
|
|
@ -315,6 +319,8 @@ _FINISH_REASON_MAP: Final[dict[str, OpenAIChatCompletionFinishReason]] = {
|
|||
"model_context_window_exceeded": "length",
|
||||
# Cohere
|
||||
"COMPLETE": "stop",
|
||||
"STOP_SEQUENCE": "stop",
|
||||
"TOOL_CALL": "tool_calls",
|
||||
"ERROR_TOXIC": "content_filter",
|
||||
"ERROR": "stop",
|
||||
# HuggingFace / Together AI
|
||||
|
|
|
|||
|
|
@ -146,6 +146,10 @@ def handle_anthropic_text_model_custom_llm_provider(
|
|||
return model, custom_llm_provider
|
||||
|
||||
|
||||
def declared_provider(model: str | None, custom_llm_provider: str | None = None) -> str | None:
|
||||
return custom_llm_provider or (model.split("/", 1)[0] if model and "/" in model else None)
|
||||
|
||||
|
||||
def declared_authenticating_provider(model: str | None, custom_llm_provider: str | None = None) -> str | None:
|
||||
"""The authenticating provider this pair already names, or None.
|
||||
|
||||
|
|
@ -154,7 +158,7 @@ def declared_authenticating_provider(model: str | None, custom_llm_provider: str
|
|||
and for a declared pair the resolver's answer is the declaration itself, so metadata callers
|
||||
adopt the declaration instead of resolving.
|
||||
"""
|
||||
declared: Final = custom_llm_provider or (model.split("/", 1)[0] if model and "/" in model else None)
|
||||
declared: Final = declared_provider(model, custom_llm_provider)
|
||||
return declared if declared in PROVIDERS_THAT_AUTHENTICATE_ON_PROVIDER_INFO else None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -920,6 +920,7 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
# post_call guardrails have run; the @client decorator then stores the
|
||||
# enqueue closure here instead of firing it immediately.
|
||||
self.defer_async_logging: bool = False
|
||||
self.drops_stream_token_ids: bool = False
|
||||
self.enqueue_deferred_logging: Callable[[], None] | None = None
|
||||
self._async_success_scheduled: bool = False
|
||||
self._async_success_enqueued: bool = False
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import json
|
|||
import re
|
||||
import time
|
||||
import traceback
|
||||
from collections.abc import Generator, Iterator, Mapping, Sequence
|
||||
from collections.abc import Generator, Iterable, Iterator, Mapping, Sequence
|
||||
from types import MappingProxyType
|
||||
from typing import Final, Literal, cast
|
||||
|
||||
from pydantic import ConfigDict, TypeAdapter, ValidationError, with_config
|
||||
|
|
@ -11,7 +12,12 @@ from typing_extensions import ReadOnly, TypedDict
|
|||
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.constants import RESPONSE_FORMAT_TOOL_NAME
|
||||
from litellm.constants import (
|
||||
RESPONSE_FORMAT_TOOL_NAME,
|
||||
STREAM_CHUNK_CHOICE_FIELDS,
|
||||
STREAM_FINISH_CHOICE_FIELDS,
|
||||
VLLM_CHOICE_FIELDS,
|
||||
)
|
||||
from litellm.litellm_core_utils.hidden_params import get_hidden_params
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
extract_reasoning_content,
|
||||
|
|
@ -170,6 +176,28 @@ def _split_assembled_content_for_replay(content: str | None) -> list[str]:
|
|||
return _REPLAY_CONTENT_SLICE_RE.findall(content)
|
||||
|
||||
|
||||
def _drop_choice_fields(choice: StreamingChoices, fields: Iterable[str]) -> None:
|
||||
for field in fields:
|
||||
if hasattr(choice, field):
|
||||
delattr(choice, field)
|
||||
|
||||
|
||||
def _vllm_choice_fields(choice: Mapping[str, object]) -> Mapping[str, object]:
|
||||
cached_fields: Final = _choice_provider_specific_fields(choice)
|
||||
return MappingProxyType(
|
||||
{
|
||||
field: choice[field] if field in choice else cached_fields[field]
|
||||
for field in VLLM_CHOICE_FIELDS
|
||||
if field in choice or field in cached_fields
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _choice_provider_specific_fields(choice: Mapping[str, object]) -> Mapping[str, object]:
|
||||
fields: Final = choice.get("provider_specific_fields")
|
||||
return _FIELD_MAP.validate_python(fields) if isinstance(fields, dict) else MappingProxyType({})
|
||||
|
||||
|
||||
_REPLAY_REASONING_FIELDS: Final = frozenset({"reasoning_content", "thinking_blocks", "reasoning_items"})
|
||||
_REPLAY_LEAD_FIELDS: Final = _REPLAY_REASONING_FIELDS | {"role", "provider_specific_fields"}
|
||||
_REPLAY_TOOL_FIELDS: Final = frozenset({"tool_calls", "function_call"})
|
||||
|
|
@ -220,8 +248,9 @@ def _replay_frame(
|
|||
choice.finish_reason = finish_reason
|
||||
if not keeps_choice_metadata:
|
||||
choice.logprobs = None
|
||||
if hasattr(choice, "enhancements"):
|
||||
del choice.enhancements
|
||||
_drop_choice_fields(choice, ("enhancements", *STREAM_CHUNK_CHOICE_FIELDS))
|
||||
if finish_reason is None:
|
||||
_drop_choice_fields(choice, STREAM_FINISH_CHOICE_FIELDS)
|
||||
return frame
|
||||
|
||||
|
||||
|
|
@ -325,7 +354,9 @@ async def convert_to_streaming_response_async(
|
|||
|
||||
logprobs = choice.get("logprobs", None)
|
||||
|
||||
choice = StreamingChoices(finish_reason=finish_reason, index=idx, delta=delta, logprobs=logprobs)
|
||||
choice = StreamingChoices(
|
||||
finish_reason=finish_reason, index=idx, delta=delta, logprobs=logprobs, **_vllm_choice_fields(choice)
|
||||
)
|
||||
choice_list.append(choice)
|
||||
|
||||
model_response_object.choices = choice_list
|
||||
|
|
@ -388,6 +419,7 @@ def convert_to_streaming_response(
|
|||
delta=delta,
|
||||
logprobs=logprobs,
|
||||
enhancements=enhancements,
|
||||
**_vllm_choice_fields(choice),
|
||||
)
|
||||
|
||||
choice_list.append(choice)
|
||||
|
|
@ -872,6 +904,7 @@ def convert_to_model_response_object(
|
|||
logprobs=logprobs,
|
||||
enhancements=enhancements,
|
||||
provider_specific_fields=provider_specific_fields,
|
||||
**_vllm_choice_fields(choice),
|
||||
)
|
||||
if provider_sent_logprobs and logprobs is None:
|
||||
choice.logprobs = None
|
||||
|
|
|
|||
|
|
@ -10,12 +10,18 @@ class ChatTemplateToolSection:
|
|||
preamble_tokens: int
|
||||
implicit_system_turn_tokens: int
|
||||
render_tool_schemas: Callable[[Sequence[Mapping[str, object]]], str]
|
||||
counts_tool_choice: bool = False
|
||||
renders_strict_only_when_true: bool = False
|
||||
|
||||
|
||||
def _deepseek_v4_tool_schemas(functions: Sequence[Mapping[str, object]]) -> str:
|
||||
def _json_line_tool_schemas(functions: Sequence[Mapping[str, object]]) -> str:
|
||||
return "\n".join(json.dumps(function, ensure_ascii=False) for function in functions)
|
||||
|
||||
|
||||
def _compact_json_line_tool_schemas(functions: Sequence[Mapping[str, object]]) -> str:
|
||||
return "\n".join(json.dumps(function, ensure_ascii=False, separators=(",", ":")) for function in functions)
|
||||
|
||||
|
||||
def _qwen3_5_tool_schemas(functions: Sequence[Mapping[str, object]]) -> str:
|
||||
return "".join(
|
||||
"\n" + json.dumps({"type": "function", "function": function}, ensure_ascii=False) for function in functions
|
||||
|
|
@ -23,13 +29,29 @@ def _qwen3_5_tool_schemas(functions: Sequence[Mapping[str, object]]) -> str:
|
|||
|
||||
|
||||
_TOOL_SECTIONS: Final = (
|
||||
(
|
||||
re.compile(r"deepseek-v4(?:[-._]|p)1(?!\d)", re.IGNORECASE),
|
||||
ChatTemplateToolSection(211, 0, _json_line_tool_schemas),
|
||||
),
|
||||
(
|
||||
re.compile(
|
||||
r"deepseek-v4(?![.\d]|p\d)|^(?:deepseek/)?deepseek-(?:chat|reasoner|flash)(?::free)?$", re.IGNORECASE
|
||||
),
|
||||
ChatTemplateToolSection(222, 0, _deepseek_v4_tool_schemas),
|
||||
ChatTemplateToolSection(222, 0, _json_line_tool_schemas),
|
||||
),
|
||||
(re.compile(r"qwen3[._]?5(?!\d)", re.IGNORECASE), ChatTemplateToolSection(196, 5, _qwen3_5_tool_schemas)),
|
||||
(
|
||||
re.compile(r"grok-4[-.]1(?![.\d])", re.IGNORECASE),
|
||||
ChatTemplateToolSection(
|
||||
0, 0, _compact_json_line_tool_schemas, counts_tool_choice=True, renders_strict_only_when_true=True
|
||||
),
|
||||
),
|
||||
(
|
||||
re.compile(r"grok-4[-.]3(?![.\d])", re.IGNORECASE),
|
||||
ChatTemplateToolSection(
|
||||
0, 0, _json_line_tool_schemas, counts_tool_choice=True, renders_strict_only_when_true=True
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import io
|
|||
import json
|
||||
import mimetypes
|
||||
import re
|
||||
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
|
||||
from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from graphlib import CycleError, TopologicalSorter
|
||||
from itertools import chain, groupby, islice
|
||||
|
|
@ -29,6 +29,7 @@ from litellm.types.llms.openai import (
|
|||
ChatCompletionAudioObject,
|
||||
ChatCompletionFileObject,
|
||||
ChatCompletionImageObject,
|
||||
ChatCompletionPromptCacheBreakpoint,
|
||||
ChatCompletionReasoningItem,
|
||||
ChatCompletionReasoningSummaryTextBlock,
|
||||
ChatCompletionRedactedThinkingBlock,
|
||||
|
|
@ -2193,16 +2194,62 @@ def with_prompt_cache_breakpoint(target: _MarkedT, marker: object) -> _MarkedT:
|
|||
|
||||
|
||||
_PROMPT_CACHE_BREAKPOINT: Final = TypeAdapter(PromptCacheBreakpoint)
|
||||
_CHAT_COMPLETION_PROMPT_CACHE_BREAKPOINT: Final = TypeAdapter(ChatCompletionPromptCacheBreakpoint)
|
||||
_AcceptedBreakpointT: Final = TypeVar("_AcceptedBreakpointT", bound=Mapping[str, object])
|
||||
|
||||
|
||||
def _accepted_prompt_cache_breakpoint(
|
||||
accepted: TypeAdapter[_AcceptedBreakpointT], marker: object
|
||||
) -> _AcceptedBreakpointT | None:
|
||||
try:
|
||||
return accepted.validate_python(marker)
|
||||
except ValidationError:
|
||||
return None
|
||||
|
||||
|
||||
def prompt_cache_breakpoint_within(
|
||||
accepted: TypeAdapter[_AcceptedBreakpointT], marker: object
|
||||
) -> _AcceptedBreakpointT | None:
|
||||
"""The client's marker cut down to the ``accepted`` shape: an unsupported ttl is dropped and the mode kept,
|
||||
and a marker whose mode the target cannot read is dropped whole."""
|
||||
whole: Final = _accepted_prompt_cache_breakpoint(accepted, marker)
|
||||
if whole is not None:
|
||||
return whole
|
||||
fields: Final = _breakpoint_fields_or_none(marker)
|
||||
without_ttl: Final = (
|
||||
None
|
||||
if fields is None
|
||||
else _accepted_prompt_cache_breakpoint(accepted, {key: item for key, item in fields.items() if key != "ttl"})
|
||||
)
|
||||
verbose_logger.debug(
|
||||
"Dropping malformed prompt_cache_breakpoint %r under drop_params"
|
||||
if without_ttl is None
|
||||
else "Dropping the unsupported ttl of prompt_cache_breakpoint %r under drop_params",
|
||||
marker,
|
||||
)
|
||||
return without_ttl
|
||||
|
||||
|
||||
def prompt_cache_breakpoint_for_wire(marker: object, drop_params: bool) -> object:
|
||||
if marker is None or not drop_params:
|
||||
return marker
|
||||
try:
|
||||
return _PROMPT_CACHE_BREAKPOINT.validate_python(marker)
|
||||
except ValidationError:
|
||||
verbose_logger.debug("Dropping malformed prompt_cache_breakpoint %r under drop_params", marker)
|
||||
return None
|
||||
return prompt_cache_breakpoint_within(_PROMPT_CACHE_BREAKPOINT, marker)
|
||||
|
||||
|
||||
def prompt_cache_breakpoint_with_readable_mode(marker: object, drop_params: bool) -> object:
|
||||
"""The client's marker as written, or None under drop_params when no target could read its mode, so the
|
||||
target's own config decides which ttl and keys it keeps."""
|
||||
return None if prompt_cache_breakpoint_for_wire(marker, drop_params) is None else marker
|
||||
|
||||
|
||||
def whole_prompt_cache_breakpoint_or_none(marker: object) -> PromptCacheBreakpoint | None:
|
||||
return _accepted_prompt_cache_breakpoint(_PROMPT_CACHE_BREAKPOINT, marker)
|
||||
|
||||
|
||||
def chat_completion_prompt_cache_breakpoint_for_wire(marker: object, drop_params: bool) -> object:
|
||||
if marker is None or not drop_params:
|
||||
return marker
|
||||
return prompt_cache_breakpoint_within(_CHAT_COMPLETION_PROMPT_CACHE_BREAKPOINT, marker)
|
||||
|
||||
|
||||
def cache_control_as_prompt_cache_breakpoint(block: _MarkedT) -> _MarkedT:
|
||||
|
|
@ -2220,6 +2267,13 @@ _BREAKPOINT_FIELDS: Final = TypeAdapter(Mapping[str, object])
|
|||
_BREAKPOINT_BLOCKS: Final = TypeAdapter(list[object])
|
||||
|
||||
|
||||
def _breakpoint_fields_or_none(value: object) -> Mapping[str, object] | None:
|
||||
try:
|
||||
return _BREAKPOINT_FIELDS.validate_python(value)
|
||||
except ValidationError:
|
||||
return None
|
||||
|
||||
|
||||
def _without_prompt_cache_breakpoint(value: object) -> object:
|
||||
if not isinstance(value, Mapping):
|
||||
return value
|
||||
|
|
@ -2242,6 +2296,27 @@ def without_prompt_cache_breakpoints(block: _MarkedT) -> _MarkedT:
|
|||
return cast(_MarkedT, stripped) # cast-ok: same block shape as the input minus the marker keys
|
||||
|
||||
|
||||
def _part_with_chat_completion_prompt_cache_breakpoint(part: object) -> object:
|
||||
if not isinstance(part, Mapping):
|
||||
return part
|
||||
fields: Final = _BREAKPOINT_FIELDS.validate_python(part)
|
||||
marker: Final = fields.get("prompt_cache_breakpoint")
|
||||
if marker is None:
|
||||
return fields
|
||||
unmarked: Final = {key: item for key, item in fields.items() if key != "prompt_cache_breakpoint"}
|
||||
return with_prompt_cache_breakpoint(unmarked, chat_completion_prompt_cache_breakpoint_for_wire(marker, True))
|
||||
|
||||
|
||||
def chat_completion_prompt_cache_breakpoints_for_wire(message: _MarkedT, drop_params: bool) -> _MarkedT:
|
||||
content: Final = message.get("content")
|
||||
if not drop_params or not isinstance(content, list):
|
||||
return message
|
||||
parts: Final = [
|
||||
_part_with_chat_completion_prompt_cache_breakpoint(part) for part in _BREAKPOINT_BLOCKS.validate_python(content)
|
||||
]
|
||||
return cast(_MarkedT, {**message, "content": parts}) # cast-ok: same message shape with its parts' markers trimmed
|
||||
|
||||
|
||||
LITELLM_RESPONSE_ONLY_MESSAGE_FIELDS: Final = frozenset(
|
||||
{"thinking_blocks", "reasoning_items", "provider_specific_fields"}
|
||||
)
|
||||
|
|
@ -3235,3 +3310,15 @@ def text_completion_prompt_to_messages(prompt: object) -> tuple[AllMessageValues
|
|||
if string_entries and len(string_entries) == len(entries):
|
||||
return tuple(ChatCompletionUserMessage(role="user", content=entry) for entry in string_entries)
|
||||
raise ValueError(f"`prompt` must be a non-empty string or a non-empty list of strings. Got: {prompt_type_name}.")
|
||||
|
||||
|
||||
def is_present_non_string(value: object) -> bool:
|
||||
return value is not None and not isinstance(value, str)
|
||||
|
||||
|
||||
def invalid_type_message(field_path: str, value: object) -> str:
|
||||
return f"Invalid type for '{field_path}': expected a string, got {type(value).__name__}"
|
||||
|
||||
|
||||
def is_unhashable_value_behind(error: TypeError, value: object) -> bool:
|
||||
return not isinstance(value, Hashable) and f"unhashable type: '{type(value).__name__}'" in str(error)
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ from litellm.litellm_core_utils.prompt_templates.mid_conversation_system import
|
|||
parts_of,
|
||||
place_mid_conversation_system,
|
||||
split_leading_system_run,
|
||||
string_field,
|
||||
)
|
||||
from litellm.litellm_core_utils.tool_fields import tools_without_hosted_mcp
|
||||
from litellm.litellm_core_utils.url_utils import async_safe_get, safe_get
|
||||
from litellm.llms.custom_httpx.http_handler import HTTPHandler, get_async_httpx_client, header_value
|
||||
from litellm.types.files import get_file_extension_from_mime_type
|
||||
|
|
@ -2023,7 +2025,7 @@ def _server_tool_answers_replaced_after(messages: Sequence[object], index: int)
|
|||
following: Final = _messages_until_the_next_assistant(messages, index)
|
||||
a_user_turn_remains: Final = any(
|
||||
message_field(message, "role") in ("user", "tool", "function")
|
||||
and message_field(message, "tool_call_id") not in kept_server_result_ids
|
||||
and string_field(message, "tool_call_id") not in kept_server_result_ids
|
||||
for message in following
|
||||
)
|
||||
return _tool_call_ids_in_tool_messages(following) & kept_server_result_ids if a_user_turn_remains else frozenset()
|
||||
|
|
@ -5915,8 +5917,13 @@ def function_call_prompt(
|
|||
llm_provider: str,
|
||||
drop_params: bool,
|
||||
) -> list[dict[str, object]]:
|
||||
prompt_functions: Final = tools_without_hosted_mcp(
|
||||
functions, model=model, route=llm_provider, drop_params=drop_params
|
||||
)
|
||||
if not prompt_functions:
|
||||
return messages
|
||||
header: Final = """To call a function, reply with JSON ONLY in this format {"name": "function_name", "arguments":{"argument_name": "argument_value"}}. Once a function result answers the request, reply to the user in plain text instead of calling a function again. The following functions are available to you:"""
|
||||
function_prompt: Final = header + "".join(f"\n{function}\n" for function in functions)
|
||||
function_prompt: Final = header + "".join(f"\n{function}\n" for function in prompt_functions)
|
||||
if not any(_is_system_message(message) for message in messages):
|
||||
return [*messages, {"role": "system", "content": function_prompt}]
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -85,6 +85,11 @@ def message_field(message: object, key: str) -> object:
|
|||
return mapping.get(key) if mapping is not None else getattr(message, key, None)
|
||||
|
||||
|
||||
def string_field(message: object, key: str) -> str | None:
|
||||
value: Final = message_field(message, key)
|
||||
return value if isinstance(value, str) else None
|
||||
|
||||
|
||||
def is_system_message(message: object) -> bool:
|
||||
return message_field(message, "role") == "system"
|
||||
|
||||
|
|
|
|||
|
|
@ -196,6 +196,9 @@ def _redact_function_call(function_call) -> None:
|
|||
|
||||
def _redact_choice_content(choice):
|
||||
"""Helper to redact content in a choice (message or delta)."""
|
||||
if isinstance(choice, (litellm.Choices, litellm.utils.StreamingChoices, litellm.utils.TextChoices)):
|
||||
drop_token_ids(choice)
|
||||
_redact_provider_specific_fields(choice)
|
||||
if isinstance(choice, litellm.Choices):
|
||||
if choice.message.content is not None:
|
||||
choice.message.content = REDACTED_BY_LITELLM
|
||||
|
|
@ -216,6 +219,9 @@ def _redact_choice_content(choice):
|
|||
_redact_provider_specific_fields(choice.delta)
|
||||
_redact_tool_calls(getattr(choice.delta, "tool_calls", None))
|
||||
_redact_function_call(getattr(choice.delta, "function_call", None))
|
||||
elif isinstance(choice, litellm.utils.TextChoices):
|
||||
_redact_field_when_present(choice, "text")
|
||||
_redact_field_when_present(choice, "reasoning_content")
|
||||
|
||||
|
||||
def _as_mapping(value: object) -> Mapping[str, object] | None:
|
||||
|
|
@ -227,14 +233,27 @@ def _redact_provider_specific_fields(message: object) -> None:
|
|||
setattr(message, "provider_specific_fields", _redacted_provider_specific_fields(fields)) # noqa: B010 # typed as object
|
||||
|
||||
|
||||
def _drop_provider_specific_token_ids(holder: object) -> None:
|
||||
fields: Final = _as_mapping(getattr(holder, "provider_specific_fields", None))
|
||||
if fields is not None and fields.get("token_ids") is not None:
|
||||
setattr(holder, "provider_specific_fields", {**fields, "token_ids": None}) # noqa: B010 # typed as object
|
||||
|
||||
|
||||
def drop_token_ids(choice: object) -> None:
|
||||
if getattr(choice, "token_ids", None) is not None:
|
||||
setattr(choice, "token_ids", None) # noqa: B010 # pydantic extra, not a declared field
|
||||
for holder in (choice, getattr(choice, "delta", None), getattr(choice, "message", None)):
|
||||
_drop_provider_specific_token_ids(holder)
|
||||
|
||||
|
||||
def _redact_provider_specific_fields_dict(choice: Mapping[str, object]) -> None:
|
||||
for message in (choice.get("message"), choice.get("delta")):
|
||||
for holder in (choice, choice.get("message"), choice.get("delta")):
|
||||
if (
|
||||
(readable := _as_mapping(message)) is not None
|
||||
(readable := _as_mapping(holder)) is not None
|
||||
and (fields := _as_mapping(readable.get("provider_specific_fields"))) is not None
|
||||
and isinstance(message, dict)
|
||||
and isinstance(holder, dict)
|
||||
):
|
||||
message["provider_specific_fields"] = _redacted_provider_specific_fields(fields)
|
||||
holder["provider_specific_fields"] = _redacted_provider_specific_fields(fields)
|
||||
|
||||
|
||||
def _redacted_provider_specific_fields(fields: Mapping[str, object]) -> Mapping[str, object]:
|
||||
|
|
@ -251,6 +270,7 @@ def _redacted_provider_specific_fields(fields: Mapping[str, object]) -> Mapping[
|
|||
"compaction_blocks",
|
||||
"compaction_start",
|
||||
"compaction_delta",
|
||||
"token_ids",
|
||||
)
|
||||
if key in fields
|
||||
},
|
||||
|
|
@ -422,6 +442,8 @@ def _redact_model_response_dict_choices(choices, redacted_str: str):
|
|||
for choice in choices:
|
||||
if isinstance(choice, dict):
|
||||
_redact_provider_specific_fields_dict(choice)
|
||||
if choice.get("token_ids") is not None:
|
||||
choice["token_ids"] = None
|
||||
if "message" in choice and isinstance(choice["message"], dict):
|
||||
if choice["message"].get("content") is not None:
|
||||
choice["message"]["content"] = redacted_str
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from collections.abc import Iterable
|
||||
from typing import Final
|
||||
|
||||
from litellm.constants import VLLM_CHOICE_FIELDS
|
||||
from litellm.types.utils import ModelResponseStream, StreamingChoices
|
||||
|
||||
|
||||
|
|
@ -9,12 +10,19 @@ def is_empty_streaming_choice(choice: StreamingChoices) -> bool:
|
|||
return False
|
||||
if getattr(choice, "logprobs", None) is not None:
|
||||
return False
|
||||
if not _all_values_none(_vllm_choice_values(choice)):
|
||||
return False
|
||||
delta: Final = choice.delta if hasattr(choice, "delta") else None
|
||||
if delta is None:
|
||||
return True
|
||||
return _all_values_none(delta.model_dump().values())
|
||||
|
||||
|
||||
def _vllm_choice_values(choice: StreamingChoices) -> Iterable[object]:
|
||||
choice_extras: Final = choice.model_extra or {}
|
||||
return (choice_extras.get(field) for field in VLLM_CHOICE_FIELDS)
|
||||
|
||||
|
||||
def _all_values_none(values: Iterable[object]) -> bool:
|
||||
return all(value is None for value in values)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from typing_extensions import ReadOnly, Required
|
|||
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.constants import VLLM_CHOICE_FIELDS
|
||||
from litellm.litellm_core_utils.core_helpers import (
|
||||
PROVIDERS_BILLED_FROM_THEIR_OWN_USAGE_FIELDS,
|
||||
UPSTREAM_USAGE_COST,
|
||||
|
|
@ -31,6 +32,7 @@ from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response impo
|
|||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
get_content_from_model_response, # pyright: ignore[reportUnknownVariableType] # response param accepts an untyped dict
|
||||
)
|
||||
from litellm.litellm_core_utils.redact_messages import drop_token_ids
|
||||
from litellm.litellm_core_utils.token_counter import usage_tokenizer_model
|
||||
from litellm.types.llms.openai import (
|
||||
AllMessageValues,
|
||||
|
|
@ -434,6 +436,8 @@ class ChunkProcessor:
|
|||
chunks, unreported="tool_calls" if _carries_tool_calls(chunks) else "stop"
|
||||
),
|
||||
message={"role": ChunkProcessor._get_role_from_chunks(chunks), "content": ""},
|
||||
provider_specific_fields=None,
|
||||
**_streamed_vllm_choice_fields(chain.from_iterable(chunk.get("choices") or () for chunk in chunks)),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -504,6 +508,7 @@ class ChunkProcessor:
|
|||
"index": 0,
|
||||
"message": {"role": role, "content": ""},
|
||||
"finish_reason": finish_reason,
|
||||
**_streamed_vllm_choice_fields(_first_choices(chunks)),
|
||||
}
|
||||
]
|
||||
),
|
||||
|
|
@ -1371,6 +1376,7 @@ def _rebuilt_text_choice(index: int, choices: Sequence[TextChoices]) -> TextChoi
|
|||
index=index,
|
||||
logprobs=_merged_logprobs(choices),
|
||||
finish_reason=next((choice.finish_reason for choice in reversed(choices) if choice.finish_reason), None),
|
||||
**_streamed_vllm_choice_fields(choices),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1504,6 +1510,31 @@ def _field_of(value: object, name: str) -> object:
|
|||
return getattr(value, name, None)
|
||||
|
||||
|
||||
def _first_choices(chunks: Iterable[object]) -> tuple[object, ...]:
|
||||
return tuple(next(iter(object_items(_field_of(chunk, "choices"))), None) for chunk in chunks)
|
||||
|
||||
|
||||
def _choice_extras(choice: object) -> Mapping[str, object]:
|
||||
if isinstance(choice, BaseModel):
|
||||
return choice.model_extra or {}
|
||||
return object_mapping(choice) or {}
|
||||
|
||||
|
||||
def _streamed_vllm_choice_fields(choices: Iterable[object]) -> Mapping[str, object]:
|
||||
extras: Final = tuple(map(_choice_extras, choices))
|
||||
token_id_runs: Final = tuple(object_items(fields.get("token_ids")) for fields in extras)
|
||||
stop_reasons: Final = tuple(reason for fields in extras if (reason := fields.get("stop_reason")) is not None)
|
||||
has_token_ids: Final = any(fields.get("token_ids") is not None for fields in extras)
|
||||
streamed_values: Final[Mapping[str, object]] = MappingProxyType(
|
||||
{
|
||||
"token_ids": list(chain.from_iterable(token_id_runs)) if has_token_ids else None,
|
||||
"stop_reason": stop_reasons[-1] if stop_reasons else None,
|
||||
}
|
||||
)
|
||||
present_fields: Final = frozenset(chain.from_iterable(fields.keys() for fields in extras))
|
||||
return MappingProxyType({field: streamed_values[field] for field in VLLM_CHOICE_FIELDS if field in present_fields})
|
||||
|
||||
|
||||
def _delta_reasoning_items(delta: object) -> Sequence[object]:
|
||||
items: Final = _field_of(delta, "reasoning_items")
|
||||
return cast(Sequence[object], items) if isinstance(items, list) else () # cast-ok: items forwarded verbatim
|
||||
|
|
@ -1776,6 +1807,19 @@ def stream_chunk_builder(
|
|||
end_time: object = None,
|
||||
logging_obj: Optional["Logging"] = None,
|
||||
count_prompt_tokens: Callable[[], int] | None = None,
|
||||
) -> ModelResponse | TextCompletionResponse | None:
|
||||
response: Final = _assembled_stream_response(chunks, messages, logging_obj, count_prompt_tokens)
|
||||
if response is not None and getattr(logging_obj, "drops_stream_token_ids", False) is True:
|
||||
for choice in response.choices:
|
||||
drop_token_ids(choice)
|
||||
return response
|
||||
|
||||
|
||||
def _assembled_stream_response(
|
||||
chunks: Sequence[object],
|
||||
messages: Sequence[AllMessageValues | Message] | None,
|
||||
logging_obj: Optional["Logging"],
|
||||
count_prompt_tokens: Callable[[], int] | None,
|
||||
) -> ModelResponse | TextCompletionResponse | None:
|
||||
try:
|
||||
if chunks is None:
|
||||
|
|
@ -1788,7 +1832,7 @@ def stream_chunk_builder(
|
|||
if not chunks:
|
||||
return None
|
||||
|
||||
processor: Final = ChunkProcessor(chunks, messages)
|
||||
processor: Final = ChunkProcessor(list(chunks), messages)
|
||||
chunks = processor.chunks
|
||||
|
||||
### BASE-CASE ###
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from typing_extensions import NotRequired, TypedDict
|
|||
import litellm
|
||||
from litellm import verbose_logger
|
||||
from litellm._uuid import uuid
|
||||
from litellm.constants import STREAM_CHUNK_CHOICE_FIELDS, STREAM_FINISH_CHOICE_FIELDS
|
||||
from litellm.litellm_core_utils.asyncify import asyncify
|
||||
from litellm.litellm_core_utils.core_helpers import (
|
||||
UPSTREAM_USAGE_COST,
|
||||
|
|
@ -37,10 +38,11 @@ from litellm.litellm_core_utils.hidden_params import (
|
|||
set_hidden_param,
|
||||
stored_attribute,
|
||||
)
|
||||
from litellm.litellm_core_utils.llm_cost_calc.guardrail_cost import object_items
|
||||
from litellm.litellm_core_utils.model_response_utils import (
|
||||
is_model_response_stream_empty,
|
||||
)
|
||||
from litellm.litellm_core_utils.redact_messages import LiteLLMLoggingObject
|
||||
from litellm.litellm_core_utils.redact_messages import LiteLLMLoggingObject, drop_token_ids
|
||||
from litellm.litellm_core_utils.thread_pool_executor import executor
|
||||
from litellm.llms.ollama.common_utils import OllamaResponseIdNormalizer
|
||||
from litellm.types.llms.base import LiteLLMBaseModel
|
||||
|
|
@ -137,6 +139,70 @@ def _prepend_upstream_choice_extras(
|
|||
choice.__pydantic_fields_set__.update(upstream_extras)
|
||||
|
||||
|
||||
_NO_UPSTREAM_CHOICE_FIELDS: Final[Mapping[str, object]] = MappingProxyType({})
|
||||
_NO_CHOICE_FIELDS_BY_INDEX: Final[Mapping[int, Mapping[str, object]]] = MappingProxyType({})
|
||||
|
||||
|
||||
def _upstream_first_choice(upstream_chunk: object) -> BaseModel | None:
|
||||
match getattr(upstream_chunk, "choices", None):
|
||||
case [BaseModel() as first_choice, *_]:
|
||||
return first_choice
|
||||
case _:
|
||||
return None
|
||||
|
||||
|
||||
def _upstream_choice_fields(upstream_chunk: object, fields: Sequence[str]) -> Mapping[str, object]:
|
||||
first_choice: Final = _upstream_first_choice(upstream_chunk)
|
||||
if first_choice is None:
|
||||
return _NO_UPSTREAM_CHOICE_FIELDS
|
||||
upstream_extras: Final[Mapping[str, object]] = first_choice.model_extra or {}
|
||||
return MappingProxyType({field: upstream_extras[field] for field in fields if field in upstream_extras})
|
||||
|
||||
|
||||
def _upstream_choice_index(upstream_chunk: object) -> int:
|
||||
upstream_index: Final[object] = getattr(_upstream_first_choice(upstream_chunk), "index", 0)
|
||||
return upstream_index if isinstance(upstream_index, int) else 0
|
||||
|
||||
|
||||
def _response_choice_index(response_obj: Mapping[str, object]) -> int:
|
||||
choice_index: Final = response_obj.get("choice_index")
|
||||
if isinstance(choice_index, int):
|
||||
return choice_index
|
||||
return _upstream_choice_index(response_obj.get("original_chunk"))
|
||||
|
||||
|
||||
def _non_null_upstream_choice_fields(upstream_chunk: object, fields: Sequence[str]) -> Mapping[str, object]:
|
||||
first_choice: Final = _upstream_first_choice(upstream_chunk)
|
||||
upstream_extras: Final = first_choice.model_extra if first_choice is not None else None
|
||||
if not upstream_extras:
|
||||
return _NO_UPSTREAM_CHOICE_FIELDS
|
||||
return MappingProxyType(
|
||||
{field: upstream_extras[field] for field in fields if upstream_extras.get(field) is not None}
|
||||
)
|
||||
|
||||
|
||||
def _apply_choice_fields(choice: StreamingChoices, fields: Mapping[str, object]) -> None:
|
||||
for field, value in fields.items():
|
||||
setattr(choice, field, value)
|
||||
|
||||
|
||||
def _choice_fields_chunk(
|
||||
model_response: ModelResponseStream, upstream_index: int, fields: Mapping[str, object]
|
||||
) -> ModelResponseStream:
|
||||
model_response.choices[0].delta = Delta(content="") # rebind-ok: chunks are built in place here
|
||||
model_response.choices[0].index = upstream_index # rebind-ok: chunks are built in place here
|
||||
_apply_choice_fields(model_response.choices[0], fields)
|
||||
return model_response
|
||||
|
||||
|
||||
def _without_token_ids_under_post_call_rules(chunk: ModelResponseStream | None) -> ModelResponseStream | None:
|
||||
if chunk is None or not litellm.post_call_rules:
|
||||
return chunk
|
||||
for choice in chunk.choices:
|
||||
drop_token_ids(choice)
|
||||
return chunk
|
||||
|
||||
|
||||
def _holds_only_scalars(fields: Iterable[tuple[str, object]]) -> bool:
|
||||
return all(value is None or isinstance(value, (str, int, float, bool)) for _, value in fields)
|
||||
|
||||
|
|
@ -378,6 +444,8 @@ class CustomStreamWrapper:
|
|||
self._provider_response_model: str | None = None
|
||||
self._upstream_provider: str | None = None
|
||||
self.received_finish_reason: str | None = None
|
||||
self.received_finish_choice_fields: Mapping[int, Mapping[str, object]] = _NO_CHOICE_FIELDS_BY_INDEX
|
||||
self.carried_chunk_choice_fields: Mapping[int, Mapping[str, object]] = _NO_CHOICE_FIELDS_BY_INDEX
|
||||
self.intermittent_finish_reason: str | None = None # finish reasons that show up mid-stream
|
||||
self.received_any_finish_reason = False # a later chunk can reset intermittent_finish_reason
|
||||
self.special_tokens = [
|
||||
|
|
@ -823,6 +891,7 @@ class CustomStreamWrapper:
|
|||
"logprobs": logprobs,
|
||||
"original_chunk": str_line,
|
||||
"usage": usage,
|
||||
"chunk_choice_fields": _non_null_upstream_choice_fields(str_line, STREAM_CHUNK_CHOICE_FIELDS),
|
||||
}
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
|
@ -847,7 +916,7 @@ class CustomStreamWrapper:
|
|||
except Exception as e:
|
||||
raise e
|
||||
|
||||
def handle_openai_text_completion_chunk(self, chunk):
|
||||
def handle_openai_text_completion_chunk(self, chunk: object):
|
||||
try:
|
||||
text = ""
|
||||
is_finished = False
|
||||
|
|
@ -860,11 +929,15 @@ class CustomStreamWrapper:
|
|||
is_finished = True
|
||||
finish_reason = choices[0].finish_reason
|
||||
usage = getattr(chunk, "usage", None)
|
||||
choice_index: Final = _upstream_choice_index(chunk)
|
||||
return {
|
||||
"text": text,
|
||||
"is_finished": is_finished,
|
||||
"finish_reason": finish_reason,
|
||||
"usage": usage,
|
||||
"chunk_choice_fields": _non_null_upstream_choice_fields(chunk, STREAM_CHUNK_CHOICE_FIELDS),
|
||||
"choice_index": choice_index,
|
||||
"sibling_finish_reason": finish_reason if choice_index != 0 else None,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -905,13 +978,21 @@ class CustomStreamWrapper:
|
|||
except Exception as e:
|
||||
raise e
|
||||
|
||||
def _served_openrouter_model(self) -> str | None:
|
||||
if self.custom_llm_provider == LlmProviders.OPENROUTER.value or (
|
||||
self.custom_llm_provider == "cached_response"
|
||||
and self.logging_obj.model_call_details.get("custom_llm_provider") == LlmProviders.OPENROUTER.value
|
||||
):
|
||||
return self._provider_response_model
|
||||
return None
|
||||
|
||||
def model_response_creator(
|
||||
self,
|
||||
chunk: dict | None = None,
|
||||
hidden_params: Mapping[str, object] | None = None,
|
||||
delta: Delta | None = None,
|
||||
) -> ModelResponseStream:
|
||||
_model: Final = self._cached_model_name
|
||||
_model: Final = self._served_openrouter_model() or self._cached_model_name
|
||||
_logging_obj_llm_provider: Final = self._cached_logging_llm_provider
|
||||
|
||||
if chunk is None:
|
||||
|
|
@ -1175,6 +1256,13 @@ class CustomStreamWrapper:
|
|||
)
|
||||
|
||||
is_chunk_non_empty: Final = self.is_chunk_non_empty(completion_obj, model_response, response_obj)
|
||||
upstream_chunk_choice_fields: Final[Mapping[str, object]] = (
|
||||
_NO_UPSTREAM_CHOICE_FIELDS
|
||||
if litellm.post_call_rules
|
||||
else response_obj.get("chunk_choice_fields", _NO_UPSTREAM_CHOICE_FIELDS)
|
||||
)
|
||||
chunk_choice_fields: Final = self._take_carried_chunk_choice_fields(response_obj, upstream_chunk_choice_fields)
|
||||
sibling_finish_reason: Final[str | None] = response_obj.get("sibling_finish_reason")
|
||||
|
||||
if is_chunk_non_empty: # cannot set content of an OpenAI Object to be an empty string
|
||||
self.raise_on_model_repetition()
|
||||
|
|
@ -1262,16 +1350,40 @@ class CustomStreamWrapper:
|
|||
if response_obj.get("provider_specific_fields") is not None:
|
||||
completion_obj["provider_specific_fields"] = response_obj["provider_specific_fields"]
|
||||
model_response.choices[0].delta = Delta(**completion_obj)
|
||||
_index: Final[int | None] = completion_obj.get("index")
|
||||
_index: Final[int | None] = completion_obj.get("index", response_obj.get("choice_index"))
|
||||
if _index is not None:
|
||||
model_response.choices[0].index = _index
|
||||
|
||||
_apply_choice_fields(model_response.choices[0], chunk_choice_fields)
|
||||
if sibling_finish_reason is not None:
|
||||
self._finish_sibling_choice(model_response.choices[0], sibling_finish_reason)
|
||||
elif response_obj.get("is_finished"):
|
||||
self._apply_finish_choice_fields(model_response.choices[0])
|
||||
self._optional_combine_thinking_block_in_choices(model_response=model_response)
|
||||
|
||||
return model_response
|
||||
else:
|
||||
return
|
||||
elif sibling_finish_reason is not None:
|
||||
sibling_chunk: Final = _choice_fields_chunk(
|
||||
model_response, _response_choice_index(response_obj), chunk_choice_fields
|
||||
)
|
||||
self._finish_sibling_choice(sibling_chunk.choices[0], sibling_finish_reason)
|
||||
return sibling_chunk
|
||||
elif self.received_finish_reason is not None:
|
||||
chunk_upstream_index: Final = _response_choice_index(response_obj)
|
||||
if chunk_choice_fields and self._streams_past_the_pending_finish(
|
||||
model_response, response_obj, chunk_upstream_index
|
||||
):
|
||||
self.carried_chunk_choice_fields = MappingProxyType(
|
||||
{**self.carried_chunk_choice_fields, chunk_upstream_index: chunk_choice_fields}
|
||||
)
|
||||
else:
|
||||
another_choice_fields_chunk: Final = self._another_choice_fields_chunk(
|
||||
model_response, response_obj, chunk_choice_fields
|
||||
)
|
||||
if another_choice_fields_chunk is not None:
|
||||
return another_choice_fields_chunk
|
||||
if self.sent_last_chunk is True:
|
||||
# Bedrock returns the guardrail trace in the last chunk - we want to return this here
|
||||
if self.custom_llm_provider == "bedrock" and "trace" in model_response:
|
||||
|
|
@ -1309,12 +1421,20 @@ class CustomStreamWrapper:
|
|||
model_response.choices[0].finish_reason = self._normalize_tool_call_finish_reason(
|
||||
map_finish_reason(finish_reason=self.received_finish_reason)
|
||||
) # ensure consistent output to openai
|
||||
self._apply_finish_choice_fields(model_response.choices[0])
|
||||
if (
|
||||
response_obj.get("is_finished")
|
||||
and _response_choice_index(response_obj) == model_response.choices[0].index
|
||||
):
|
||||
_apply_choice_fields(model_response.choices[0], chunk_choice_fields)
|
||||
|
||||
self.sent_last_chunk = True
|
||||
|
||||
return model_response
|
||||
elif self._has_special_delta_content(model_response):
|
||||
return self._handle_special_delta_content(model_response)
|
||||
elif chunk_choice_fields:
|
||||
return _choice_fields_chunk(model_response, _response_choice_index(response_obj), chunk_choice_fields)
|
||||
else:
|
||||
if hasattr(model_response, "usage"):
|
||||
self.chunks.append(model_response)
|
||||
|
|
@ -1537,6 +1657,8 @@ class CustomStreamWrapper:
|
|||
completion_obj["content"] = response_obj["text"]
|
||||
print_verbose(f"completion obj content: {completion_obj['content']}")
|
||||
if response_obj["is_finished"]:
|
||||
self._record_finish_choice_fields(chunk)
|
||||
if response_obj["is_finished"] and response_obj["sibling_finish_reason"] is None:
|
||||
self.received_finish_reason = response_obj["finish_reason"]
|
||||
if response_obj["usage"] is not None:
|
||||
_text_completion_usage: Final[Usage] = response_obj["usage"]
|
||||
|
|
@ -1600,6 +1722,7 @@ class CustomStreamWrapper:
|
|||
self.system_fingerprint = cached_chunk.system_fingerprint
|
||||
if response_obj["is_finished"]:
|
||||
self.received_finish_reason = response_obj["finish_reason"]
|
||||
self._record_finish_choice_fields(cached_chunk)
|
||||
else: # openai / azure chat model
|
||||
self.uses_openai_chat_stream = True
|
||||
if isinstance(chunk, ChatCompletionChunk):
|
||||
|
|
@ -1629,6 +1752,7 @@ class CustomStreamWrapper:
|
|||
f"{self.custom_llm_provider} raised a streaming error - finish_reason: error, no content string given. Received Chunk={response_obj}"
|
||||
)
|
||||
self.received_finish_reason = response_obj["finish_reason"]
|
||||
self._record_finish_choice_fields(chunk)
|
||||
if response_obj.get("original_chunk", None) is not None:
|
||||
if hasattr(response_obj["original_chunk"], "id"):
|
||||
model_response = self.set_model_id(response_obj["original_chunk"].id, model_response)
|
||||
|
|
@ -1696,10 +1820,10 @@ class CustomStreamWrapper:
|
|||
completion_obj=completion_obj,
|
||||
)
|
||||
if isinstance(dispatch_result, _ProviderChunkEarlyReturn):
|
||||
return dispatch_result.value
|
||||
return _without_token_ids_under_post_call_rules(dispatch_result.value)
|
||||
response_obj = dispatch_result.response_obj
|
||||
|
||||
model_response.model = self.model
|
||||
model_response.model = self._served_openrouter_model() or self.model
|
||||
## FUNCTION CALL PARSING
|
||||
original_chunk: Final[_UpstreamChunkLike | None] = (
|
||||
response_obj.get("original_chunk") if response_obj is not None else None
|
||||
|
|
@ -1786,7 +1910,7 @@ class CustomStreamWrapper:
|
|||
response_obj=response_obj,
|
||||
plain_delta_created=plain_delta_created,
|
||||
)
|
||||
return result
|
||||
return _without_token_ids_under_post_call_rules(result)
|
||||
|
||||
except StopIteration:
|
||||
raise StopIteration
|
||||
|
|
@ -1979,6 +2103,75 @@ class CustomStreamWrapper:
|
|||
return "tool_calls"
|
||||
return finish_reason
|
||||
|
||||
def _record_finish_choice_fields(self, upstream_chunk: object) -> None:
|
||||
self.received_finish_choice_fields = MappingProxyType(
|
||||
{
|
||||
**self.received_finish_choice_fields,
|
||||
_upstream_choice_index(upstream_chunk): _upstream_choice_fields(
|
||||
upstream_chunk, STREAM_FINISH_CHOICE_FIELDS
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
def _apply_finish_choice_fields(self, choice: StreamingChoices) -> None:
|
||||
_apply_choice_fields(choice, self.received_finish_choice_fields.get(choice.index, _NO_UPSTREAM_CHOICE_FIELDS))
|
||||
|
||||
def _finish_sibling_choice(self, choice: StreamingChoices, finish_reason: str) -> None:
|
||||
choice.finish_reason = map_finish_reason(finish_reason) # rebind-ok: chunks are built in place here
|
||||
self._apply_finish_choice_fields(choice)
|
||||
|
||||
def _streams_past_the_pending_finish(
|
||||
self, model_response: ModelResponseStream, response_obj: Mapping[str, object], upstream_index: int
|
||||
) -> bool:
|
||||
finishing_index: Final = model_response.choices[0].index
|
||||
return (
|
||||
not self.sent_last_chunk
|
||||
and not response_obj.get("is_finished")
|
||||
and upstream_index != finishing_index
|
||||
and finishing_index in self.received_finish_choice_fields
|
||||
)
|
||||
|
||||
def _take_carried_chunk_choice_fields(
|
||||
self, response_obj: Mapping[str, object], chunk_choice_fields: Mapping[str, object]
|
||||
) -> Mapping[str, object]:
|
||||
if not self.carried_chunk_choice_fields:
|
||||
return chunk_choice_fields
|
||||
upstream_index: Final = _response_choice_index(response_obj)
|
||||
carried: Final = self.carried_chunk_choice_fields.get(upstream_index)
|
||||
if carried is None:
|
||||
return chunk_choice_fields
|
||||
self.carried_chunk_choice_fields = MappingProxyType(
|
||||
{index: fields for index, fields in self.carried_chunk_choice_fields.items() if index != upstream_index}
|
||||
)
|
||||
return MappingProxyType(
|
||||
{
|
||||
field: [*object_items(carried.get(field)), *object_items(chunk_choice_fields.get(field))]
|
||||
for field in STREAM_CHUNK_CHOICE_FIELDS
|
||||
if field in carried or field in chunk_choice_fields
|
||||
}
|
||||
)
|
||||
|
||||
def _another_choice_fields_chunk(
|
||||
self,
|
||||
model_response: ModelResponseStream,
|
||||
response_obj: Mapping[str, object],
|
||||
chunk_choice_fields: Mapping[str, object],
|
||||
) -> ModelResponseStream | None:
|
||||
upstream_index: Final = _response_choice_index(response_obj)
|
||||
is_finished: Final = bool(response_obj.get("is_finished"))
|
||||
if is_finished and upstream_index == model_response.choices[0].index and not self.sent_last_chunk:
|
||||
return None
|
||||
finish_fields: Final = (
|
||||
self.received_finish_choice_fields.get(upstream_index, _NO_UPSTREAM_CHOICE_FIELDS)
|
||||
if is_finished
|
||||
else _NO_UPSTREAM_CHOICE_FIELDS
|
||||
)
|
||||
if not chunk_choice_fields and all(value is None for value in finish_fields.values()):
|
||||
return None
|
||||
return _choice_fields_chunk(
|
||||
model_response, upstream_index, MappingProxyType({**finish_fields, **chunk_choice_fields})
|
||||
)
|
||||
|
||||
def finish_reason_handler(self):
|
||||
model_response: Final = self.model_response_creator(
|
||||
hidden_params=None if self.received_model_id is None else {"received_model_id": self.received_model_id}
|
||||
|
|
@ -1997,6 +2190,7 @@ class CustomStreamWrapper:
|
|||
model_response.choices[0].finish_reason = self._normalize_tool_call_finish_reason(
|
||||
_finish_reason if _finish_reason is not None else "stop"
|
||||
)
|
||||
self._apply_finish_choice_fields(model_response.choices[0])
|
||||
close_tag: Final = self._close_open_think_block()
|
||||
if close_tag is not None:
|
||||
model_response.choices[0].delta.content = close_tag
|
||||
|
|
@ -2561,7 +2755,7 @@ class CustomStreamWrapper:
|
|||
if partial is None or not isinstance(usage, Usage):
|
||||
return None
|
||||
if self.model:
|
||||
partial.model = self.model
|
||||
partial.model = self._served_openrouter_model() or self.model
|
||||
backfill_missing_cache_usage_fields(usage)
|
||||
return partial
|
||||
|
||||
|
|
|
|||
|
|
@ -990,7 +990,11 @@ def _count_extra(
|
|||
tool_section (Optional[ChatTemplateToolSection]): How the model's chat template renders tools, if known.
|
||||
"""
|
||||
reply_primer_tokens: Final = 3
|
||||
tool_choice_tokens: Final = 0 if tool_section is not None else _count_tool_choice(count_function, tool_choice)
|
||||
tool_choice_tokens: Final = (
|
||||
_count_tool_choice(count_function, tool_choice)
|
||||
if tool_section is None or tool_section.counts_tool_choice
|
||||
else 0
|
||||
)
|
||||
return (
|
||||
reply_primer_tokens
|
||||
+ _count_tool_definitions(count_function, tools, includes_system_message, tool_section)
|
||||
|
|
@ -1029,14 +1033,29 @@ def _count_tool_choice(
|
|||
return 0
|
||||
|
||||
|
||||
def _named_function_definitions(tools: Sequence[object]) -> Iterable[Mapping[str, object]]:
|
||||
return (function for function in _function_definitions(_tools_sent_as_functions(tools)) if function.get("name"))
|
||||
|
||||
|
||||
def sends_function_tools(tools: Sequence[object] | None) -> bool:
|
||||
return tools is not None and any(_named_function_definitions(tools))
|
||||
|
||||
|
||||
def _without_false_strict(function: Mapping[str, object]) -> Mapping[str, object]:
|
||||
return {key: value for key, value in function.items() if key != "strict" or value}
|
||||
|
||||
|
||||
def _count_chat_template_tools(
|
||||
count_function: TokenCounterFunction,
|
||||
tools: Sequence[object],
|
||||
includes_system_message: bool,
|
||||
tool_section: ChatTemplateToolSection,
|
||||
) -> int:
|
||||
functions: Final = tuple(
|
||||
function for function in _function_definitions(_tools_sent_as_functions(tools)) if function.get("name")
|
||||
named_functions: Final = tuple(_named_function_definitions(tools))
|
||||
functions: Final = (
|
||||
tuple(map(_without_false_strict, named_functions))
|
||||
if tool_section.renders_strict_only_when_true
|
||||
else named_functions
|
||||
)
|
||||
if not functions:
|
||||
return 0
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
from collections.abc import Callable, Mapping
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from copy import copy
|
||||
from dataclasses import dataclass
|
||||
from functools import reduce
|
||||
from typing import Annotated, Final, TypeAlias
|
||||
from typing import Annotated, Final, TypeAlias, TypeVar
|
||||
|
||||
from pydantic import BeforeValidator, ConfigDict, Json, TypeAdapter, ValidationError
|
||||
from typing_extensions import ReadOnly, TypedDict
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.core_helpers import normalize_drop_params
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.exceptions import UnsupportedParamsError
|
||||
from litellm.litellm_core_utils.core_helpers import drops_unsupported_params, normalize_drop_params
|
||||
|
||||
ROUTES_WITH_TOOLS: Final = frozenset(
|
||||
{
|
||||
|
|
@ -162,3 +165,145 @@ def resolve_deployment_tool_fields(
|
|||
if "proxy_server_request" not in kwargs or route not in ROUTES_WITH_TOOLS:
|
||||
return kwargs
|
||||
return resolve_tool_fields(kwargs, lambda: (drop_policy(kwargs),))
|
||||
|
||||
|
||||
_ToolT: Final = TypeVar("_ToolT")
|
||||
|
||||
HOSTED_MCP_DROP_WARNING: Final = (
|
||||
"Dropping the hosted MCP tool(s) %s for model=%s on %s (drop_params=True) because this route cannot run a "
|
||||
"hosted MCP tool."
|
||||
)
|
||||
|
||||
|
||||
def _hosted_mcp_server_label(tool: object) -> str | None:
|
||||
match tool:
|
||||
case {"type": "mcp", "server_label": str() as server_label}:
|
||||
return server_label
|
||||
case {"type": "mcp"}:
|
||||
return "unlabeled"
|
||||
case _:
|
||||
return None
|
||||
|
||||
|
||||
def _drop_params_hint(dropped: str) -> str:
|
||||
return (
|
||||
f"To drop {dropped} instead, set `litellm.drop_params=True` or for proxy:\n\n"
|
||||
"`litellm_settings:\n drop_params: true`\n"
|
||||
)
|
||||
|
||||
|
||||
def _one_log_line(text: str) -> str:
|
||||
return text.replace("\r", "").replace("\n", "")
|
||||
|
||||
|
||||
def _unrunnable_mcp_error(message: str, model: str, route: str) -> UnsupportedParamsError:
|
||||
return UnsupportedParamsError(message=message, model=model, llm_provider=route, status_code=400)
|
||||
|
||||
|
||||
def tools_without_hosted_mcp(tools: Sequence[_ToolT], model: str, route: str, drop_params: bool) -> Sequence[_ToolT]:
|
||||
server_labels: Final = tuple(label for label in map(_hosted_mcp_server_label, tools) if label is not None)
|
||||
if not server_labels:
|
||||
return tools
|
||||
named_servers: Final = ", ".join(f"`{label}`" for label in server_labels)
|
||||
if not drops_unsupported_params(drop_params):
|
||||
raise _unrunnable_mcp_error(
|
||||
f'{model} on {route} cannot run the hosted MCP tool {named_servers} (`type: "mcp"`). To have LiteLLM '
|
||||
"run the MCP server for this model, register it on the LiteLLM MCP gateway and send "
|
||||
'`server_url: "litellm_proxy/mcp/<server name>"`. ' + _drop_params_hint("hosted MCP tools"),
|
||||
model=model,
|
||||
route=route,
|
||||
)
|
||||
verbose_logger.warning(
|
||||
HOSTED_MCP_DROP_WARNING, _one_log_line(named_servers), _one_log_line(model), _one_log_line(route)
|
||||
)
|
||||
return [tool for tool in tools if _hosted_mcp_server_label(tool) is None]
|
||||
|
||||
|
||||
MCP_SERVERS_DROP_WARNING: Final = (
|
||||
"Dropping mcp_servers %s and their mcp_toolset tools for model=%s on %s (drop_params=True) because LiteLLM "
|
||||
"cannot pass mcp_servers to this route."
|
||||
)
|
||||
|
||||
ROUTES_WITHOUT_MCP_SERVERS: Final = frozenset(
|
||||
{"bedrock", "databricks", "deepseek", "gemini", "gigachat", "ollama", "ollama_chat", "vertex_ai", "vertex_ai_beta"}
|
||||
)
|
||||
|
||||
|
||||
def _mcp_server_name(server: object) -> str:
|
||||
match server:
|
||||
case {"name": str() as name}:
|
||||
return name
|
||||
case _:
|
||||
return "unnamed"
|
||||
|
||||
|
||||
def _mcp_server_names(mcp_servers: object) -> tuple[str, ...]:
|
||||
if not mcp_servers:
|
||||
return ()
|
||||
if not isinstance(mcp_servers, list):
|
||||
return ("unnamed",)
|
||||
return tuple(map(_mcp_server_name, _TOOLS.validate_python(mcp_servers)))
|
||||
|
||||
|
||||
def _is_mcp_toolset(tool: object) -> bool:
|
||||
match tool:
|
||||
case {"type": "mcp_toolset"}:
|
||||
return True
|
||||
case _:
|
||||
return False
|
||||
|
||||
|
||||
def drops_mcp_servers(request_kwargs: Mapping[str, object]) -> bool:
|
||||
additional_drop_params: Final = request_kwargs.get("additional_drop_params")
|
||||
return drops_unsupported_params(request_kwargs.get("drop_params")) or (
|
||||
isinstance(additional_drop_params, list) and "mcp_servers" in additional_drop_params
|
||||
)
|
||||
|
||||
|
||||
def tools_without_mcp_servers(
|
||||
mcp_servers: object, tools: Sequence[_ToolT], model: str, route: str, drop_params: bool
|
||||
) -> Sequence[_ToolT]:
|
||||
server_names: Final = _mcp_server_names(mcp_servers)
|
||||
if not server_names:
|
||||
return tools
|
||||
named_servers: Final = ", ".join(f"`{name}`" for name in server_names)
|
||||
if not drops_unsupported_params(drop_params):
|
||||
raise _unrunnable_mcp_error(
|
||||
f"LiteLLM cannot pass `mcp_servers` to {model} on {route}, so the MCP server {named_servers} would not "
|
||||
'run. To have LiteLLM run it for this model, register it on the LiteLLM MCP gateway and send a `type: "mcp"` '
|
||||
'tool with `server_url: "litellm_proxy/mcp/<server name>"`. ' + _drop_params_hint("`mcp_servers`"),
|
||||
model=model,
|
||||
route=route,
|
||||
)
|
||||
verbose_logger.warning(
|
||||
MCP_SERVERS_DROP_WARNING, _one_log_line(named_servers), _one_log_line(model), _one_log_line(route)
|
||||
)
|
||||
return [tool for tool in tools if not _is_mcp_toolset(tool)]
|
||||
|
||||
|
||||
_TOOL_SELECTION_PARAMS: Final = frozenset({"tools", "tool_choice", "parallel_tool_calls"})
|
||||
_ParamsT: Final = TypeVar("_ParamsT", bound=dict[str, object])
|
||||
|
||||
|
||||
def tool_params_without_hosted_mcp(
|
||||
params: _ParamsT, model: str, route: str, drop_params: bool, runs_hosted_mcp: Callable[[], bool] | None = None
|
||||
) -> _ParamsT:
|
||||
tools: Final = params.get("tools")
|
||||
if not isinstance(tools, list) or not tools:
|
||||
return params
|
||||
sent_tools: Final = _TOOLS.validate_python(tools)
|
||||
if all(_hosted_mcp_server_label(tool) is None for tool in sent_tools) or (
|
||||
runs_hosted_mcp is not None and runs_hosted_mcp()
|
||||
):
|
||||
return params
|
||||
kept_tools: Final = tools_without_hosted_mcp(tools=sent_tools, model=model, route=route, drop_params=drop_params)
|
||||
if len(kept_tools) == len(sent_tools):
|
||||
return params
|
||||
kept_params: Final = copy(params)
|
||||
if kept_tools:
|
||||
kept_params["tools"] = list(kept_tools)
|
||||
return kept_params
|
||||
dropped_keys: Final = ("tools",) if params.get("functions") else _TOOL_SELECTION_PARAMS
|
||||
for key in dropped_keys:
|
||||
_ = kept_params.pop(key, None)
|
||||
return kept_params
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ import copy
|
|||
import json
|
||||
import re
|
||||
import time
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
|
||||
from functools import partial
|
||||
from itertools import chain
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Final, NoReturn, cast
|
||||
|
||||
|
|
@ -11,6 +13,7 @@ from pydantic import BaseModel, ConfigDict, TypeAdapter, ValidationError
|
|||
from typing_extensions import ReadOnly, TypedDict
|
||||
|
||||
import litellm
|
||||
from litellm.anthropic_beta_headers_manager import get_unsupported_headers
|
||||
from litellm.constants import (
|
||||
ANTHROPIC_BILLING_METADATA_PREFIX,
|
||||
ANTHROPIC_MIN_THINKING_BUDGET_TOKENS,
|
||||
|
|
@ -27,7 +30,10 @@ from litellm.constants import (
|
|||
from litellm.litellm_core_utils.core_helpers import drops_unsupported_params, map_finish_reason
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
DEFS_MAX_INLINED_BYTES,
|
||||
invalid_type_message,
|
||||
is_object_schema,
|
||||
is_present_non_string,
|
||||
is_unhashable_value_behind,
|
||||
sanitize_input_schema_for_anthropic,
|
||||
schema_fragment_ref,
|
||||
schema_grammar_branch_count,
|
||||
|
|
@ -45,10 +51,13 @@ from litellm.litellm_core_utils.prompt_templates.image_handling import (
|
|||
inline_remote_image_urls,
|
||||
)
|
||||
from litellm.litellm_core_utils.prompt_templates.mid_conversation_system import (
|
||||
message_field,
|
||||
parts_of,
|
||||
place_mid_conversation_system,
|
||||
split_leading_system_run,
|
||||
)
|
||||
from litellm.litellm_core_utils.reasoning_effort_utils import OPAQUE_VALUE, warn_non_string_reasoning_effort
|
||||
from litellm.litellm_core_utils.tool_fields import tool_params_without_hosted_mcp
|
||||
from litellm.llms.base_llm.base_utils import (
|
||||
ResponseFormatSchema,
|
||||
as_json_object,
|
||||
|
|
@ -179,6 +188,76 @@ NATIVE_OUTPUT_MAX_OPTIONAL_PARAMETERS: Final = 24
|
|||
NATIVE_OUTPUT_MAX_GRAMMAR_BRANCHES: Final = 2048
|
||||
|
||||
|
||||
def _tool_call_ids_failing(
|
||||
index: int, tool_calls: object, fails: Callable[[object], bool]
|
||||
) -> Iterator[tuple[str, object]]:
|
||||
ids: Final = ((position, message_field(tool_call, "id")) for position, tool_call in enumerate(parts_of(tool_calls)))
|
||||
return (
|
||||
(f"messages[{index}].tool_calls[{position}].id", tool_call_id)
|
||||
for position, tool_call_id in ids
|
||||
if fails(tool_call_id)
|
||||
)
|
||||
|
||||
|
||||
def _roles_failing(messages: Sequence[object], fails: Callable[[object], bool]) -> Iterator[tuple[str, object]]:
|
||||
roles: Final = ((index, message_field(message, "role")) for index, message in enumerate(messages))
|
||||
return ((f"messages[{index}].role", role) for index, role in roles if fails(role))
|
||||
|
||||
|
||||
def _is_non_empty_non_string(value: object) -> bool:
|
||||
return bool(value) and is_present_non_string(value)
|
||||
|
||||
|
||||
def _assistant_tool_call_ids_failing(
|
||||
messages: Sequence[object], fails: Callable[[object], bool]
|
||||
) -> Iterator[tuple[str, object]]:
|
||||
assistant_turns: Final = (
|
||||
(index, message) for index, message in enumerate(messages) if message_field(message, "role") == "assistant"
|
||||
)
|
||||
return chain.from_iterable(
|
||||
_tool_call_ids_failing(index, message_field(message, "tool_calls"), fails) for index, message in assistant_turns
|
||||
)
|
||||
|
||||
|
||||
def _tool_answer_ids_failing(
|
||||
messages: Sequence[object], fails: Callable[[object], bool]
|
||||
) -> Iterator[tuple[str, object]]:
|
||||
answers: Final = (
|
||||
(index, message_field(message, "tool_call_id"))
|
||||
for index, message in enumerate(messages)
|
||||
if message_field(message, "role") in ("tool", "function")
|
||||
)
|
||||
return ((f"messages[{index}].tool_call_id", tool_call_id) for index, tool_call_id in answers if fails(tool_call_id))
|
||||
|
||||
|
||||
def _is_non_string_value_behind(error: AttributeError, value: object) -> bool:
|
||||
return is_present_non_string(value) and value is error.obj
|
||||
|
||||
|
||||
def _fields_behind(messages: Sequence[object], error: Exception) -> Iterable[tuple[str, object]]:
|
||||
match error:
|
||||
case TypeError():
|
||||
caused_error: Final = partial(is_unhashable_value_behind, error)
|
||||
return chain(
|
||||
_assistant_tool_call_ids_failing(messages, caused_error),
|
||||
_tool_answer_ids_failing(messages, caused_error),
|
||||
_roles_failing(messages, caused_error),
|
||||
)
|
||||
case AttributeError():
|
||||
return _assistant_tool_call_ids_failing(messages, partial(_is_non_string_value_behind, error))
|
||||
case _:
|
||||
return ()
|
||||
|
||||
|
||||
def non_string_message_field_error(messages: Sequence[object], error: Exception) -> AnthropicError | None:
|
||||
malformed_field: Final = next(
|
||||
chain(_roles_failing(messages, _is_non_empty_non_string), _fields_behind(messages, error)), None
|
||||
)
|
||||
if malformed_field is None:
|
||||
return None
|
||||
return AnthropicError(status_code=400, message=invalid_type_message(*malformed_field))
|
||||
|
||||
|
||||
def _tool_type(tool: object) -> str | None:
|
||||
match tool:
|
||||
case {"type": str() as tool_type}:
|
||||
|
|
@ -1238,6 +1317,11 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
*(tool for tool in tools[first_search_tool_index + 1 :] if not _is_openai_tool_search_tool(tool)),
|
||||
]
|
||||
|
||||
def runs_hosted_mcp(self) -> bool:
|
||||
return ANTHROPIC_BETA_HEADER_VALUES.MCP_CLIENT_2025_04_04.value not in get_unsupported_headers(
|
||||
self._resolved_provider
|
||||
)
|
||||
|
||||
def _tools_param_with_openai_tool_search_mapped(
|
||||
self, non_default_params: Mapping[str, object], model: str, drop_params: bool
|
||||
) -> Mapping[str, object]:
|
||||
|
|
@ -1826,7 +1910,14 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
model: str,
|
||||
drop_params: bool,
|
||||
) -> dict:
|
||||
is_thinking_enabled: Final = self.is_thinking_enabled(non_default_params=non_default_params)
|
||||
request_params: Final = tool_params_without_hosted_mcp(
|
||||
non_default_params,
|
||||
model=model,
|
||||
route=self._resolved_provider,
|
||||
drop_params=drop_params,
|
||||
runs_hosted_mcp=self.runs_hosted_mcp,
|
||||
)
|
||||
is_thinking_enabled: Final = self.is_thinking_enabled(non_default_params=request_params)
|
||||
|
||||
# NB: ``map_openai_params`` deliberately does NOT sanitize tool names
|
||||
# here. Names are the *original* OpenAI names at this stage, and must
|
||||
|
|
@ -1839,7 +1930,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
# via ``data = {**optional_params}`` and would 400 with
|
||||
# ``Extra inputs are not permitted``).
|
||||
|
||||
for param, value in non_default_params.items():
|
||||
for param, value in request_params.items():
|
||||
if param == "max_tokens" or param == "max_completion_tokens":
|
||||
optional_params["max_tokens"] = value if isinstance(value, int) else max(1, int(round(value)))
|
||||
elif param == "tools":
|
||||
|
|
@ -1853,8 +1944,8 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
optional_params["mcp_servers"] = mcp_servers
|
||||
elif param == "tool_choice" or param == "parallel_tool_calls":
|
||||
_tool_choice: AnthropicMessagesToolChoice | None = self._map_tool_choice(
|
||||
tool_choice=non_default_params.get("tool_choice"),
|
||||
parallel_tool_use=non_default_params.get("parallel_tool_calls"),
|
||||
tool_choice=request_params.get("tool_choice"),
|
||||
parallel_tool_use=request_params.get("parallel_tool_calls"),
|
||||
)
|
||||
|
||||
if _tool_choice is not None:
|
||||
|
|
@ -1880,7 +1971,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
if self.uses_native_structured_output_for_schema(
|
||||
model,
|
||||
self._extract_json_schema_from_response_format(value),
|
||||
REQUEST_PARAMS.validate_python(non_default_params),
|
||||
REQUEST_PARAMS.validate_python(request_params),
|
||||
):
|
||||
_output_format = self.map_response_format_to_anthropic_output_format(value)
|
||||
if _output_format is not None:
|
||||
|
|
@ -1916,7 +2007,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
# `thinking={type: enabled, budget_tokens}` interface a
|
||||
# pre-4.6 model actually supports instead of forwarding a
|
||||
# shape the model will reject.
|
||||
max_tokens = non_default_params.get("max_completion_tokens") or non_default_params.get("max_tokens")
|
||||
max_tokens = request_params.get("max_completion_tokens") or request_params.get("max_tokens")
|
||||
legacy_thinking = AnthropicConfig._map_reasoning_effort(
|
||||
reasoning_effort="medium",
|
||||
model=model,
|
||||
|
|
@ -2010,7 +2101,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
|
||||
## handle thinking tokens
|
||||
self.update_optional_params_with_thinking_tokens(
|
||||
non_default_params=non_default_params, optional_params=optional_params
|
||||
non_default_params=request_params, optional_params=optional_params
|
||||
)
|
||||
|
||||
return optional_params
|
||||
|
|
@ -2370,12 +2461,18 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
# Handling anthropic API Prompt Caching
|
||||
if len(anthropic_system_message_list) > 0:
|
||||
optional_params["system"] = anthropic_system_message_list
|
||||
conversation: Final = place_mid_conversation_system(
|
||||
later_messages,
|
||||
supports_mid_conversation_system=supports_mid_conversation_system(
|
||||
model=model, custom_llm_provider=self.custom_llm_provider
|
||||
),
|
||||
)
|
||||
try:
|
||||
conversation: Final = place_mid_conversation_system(
|
||||
later_messages,
|
||||
supports_mid_conversation_system=supports_mid_conversation_system(
|
||||
model=model, custom_llm_provider=self.custom_llm_provider
|
||||
),
|
||||
)
|
||||
except TypeError as error:
|
||||
field_error: Final = non_string_message_field_error(messages, error)
|
||||
if field_error is None:
|
||||
raise
|
||||
raise field_error from error
|
||||
# Format rest of message according to anthropic guidelines
|
||||
try:
|
||||
anthropic_messages = anthropic_messages_pt(
|
||||
|
|
@ -2385,7 +2482,8 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
drop_params=drop_params,
|
||||
)
|
||||
except Exception as e:
|
||||
raise AnthropicError(
|
||||
conversion_field_error: Final = non_string_message_field_error(messages, e)
|
||||
raise conversion_field_error or AnthropicError(
|
||||
status_code=400,
|
||||
message=f"{e}\nReceived Messages={request_messages}",
|
||||
) # don't use verbose_logger.exception, if exception is raised
|
||||
|
|
|
|||
|
|
@ -1297,7 +1297,7 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
return None
|
||||
return anthropic_beta_header.split(",")
|
||||
|
||||
def get_computer_tool_beta_header(self, computer_tool_version: str) -> str:
|
||||
def get_computer_tool_beta_header(self, computer_tool_version: str) -> str | None:
|
||||
"""
|
||||
Get the appropriate beta header for a given computer tool version.
|
||||
|
||||
|
|
@ -1305,16 +1305,14 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
computer_tool_version: The computer tool version (e.g., 'computer_20250124', 'computer_20241022')
|
||||
|
||||
Returns:
|
||||
The corresponding beta header string
|
||||
The corresponding beta header string, or None for a version with no known beta
|
||||
"""
|
||||
computer_tool_beta_mapping: Final = {
|
||||
"computer_20251124": "computer-use-2025-11-24",
|
||||
"computer_20250124": "computer-use-2025-01-24",
|
||||
"computer_20241022": "computer-use-2024-10-22",
|
||||
}
|
||||
return computer_tool_beta_mapping.get(
|
||||
computer_tool_version,
|
||||
"computer-use-2024-10-22", # Default fallback
|
||||
)
|
||||
return computer_tool_beta_mapping.get(computer_tool_version)
|
||||
|
||||
def get_anthropic_beta_list(
|
||||
self,
|
||||
|
|
@ -1346,9 +1344,8 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
if effort_used:
|
||||
betas.append(ANTHROPIC_EFFORT_BETA_HEADER) # effort-2025-11-24
|
||||
|
||||
if computer_tool_used:
|
||||
beta_header: Final = self.get_computer_tool_beta_header(computer_tool_used)
|
||||
betas.append(beta_header)
|
||||
if computer_tool_used and (computer_tool_beta := self.get_computer_tool_beta_header(computer_tool_used)):
|
||||
betas.append(computer_tool_beta)
|
||||
|
||||
# Anthropic no longer requires the prompt-caching beta header
|
||||
# Prompt caching now works automatically when cache_control is used in messages
|
||||
|
|
@ -1437,9 +1434,8 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
# Anthropic no longer requires the prompt-caching beta header
|
||||
# Prompt caching now works automatically when cache_control is used in messages
|
||||
# Reference: https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
|
||||
if computer_tool_used:
|
||||
beta_header: Final = self.get_computer_tool_beta_header(computer_tool_used)
|
||||
betas.add(beta_header)
|
||||
if computer_tool_used and (computer_tool_beta := self.get_computer_tool_beta_header(computer_tool_used)):
|
||||
betas.add(computer_tool_beta)
|
||||
# if pdf_used:
|
||||
# betas.add("pdfs-2024-09-25")
|
||||
if file_id_used:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ from litellm._logging import verbose_logger
|
|||
from litellm.litellm_core_utils.asyncify import run_async_function
|
||||
from litellm.litellm_core_utils.core_helpers import drops_unsupported_params
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObject
|
||||
from litellm.litellm_core_utils.tool_fields import (
|
||||
ROUTES_WITHOUT_MCP_SERVERS,
|
||||
drops_mcp_servers,
|
||||
tools_without_mcp_servers,
|
||||
)
|
||||
from litellm.llms.anthropic.messages.adapters.transformation import (
|
||||
AnthropicAdapter,
|
||||
)
|
||||
|
|
@ -610,6 +615,23 @@ class LiteLLMMessagesToCompletionTransformationHandler:
|
|||
Logging as LiteLLMLoggingObject,
|
||||
)
|
||||
|
||||
provider_kwargs: Final = extra_kwargs or {}
|
||||
custom_llm_provider: Final = provider_kwargs.get("custom_llm_provider")
|
||||
target_provider: Final = custom_llm_provider if isinstance(custom_llm_provider, str) else None
|
||||
drop_params: Final = drops_unsupported_params(provider_kwargs.get("drop_params"))
|
||||
strips_mcp_servers: Final = target_provider in ROUTES_WITHOUT_MCP_SERVERS
|
||||
request_tools: Final = (
|
||||
tools_without_mcp_servers(
|
||||
provider_kwargs.get("mcp_servers"),
|
||||
tools or (),
|
||||
model=model,
|
||||
route=target_provider,
|
||||
drop_params=drops_mcp_servers(provider_kwargs),
|
||||
)
|
||||
if strips_mcp_servers
|
||||
else tools
|
||||
)
|
||||
|
||||
request_data: Final[dict[str, object]] = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
|
|
@ -626,10 +648,10 @@ class LiteLLMMessagesToCompletionTransformationHandler:
|
|||
request_data["temperature"] = temperature
|
||||
if thinking:
|
||||
request_data["thinking"] = thinking
|
||||
if tool_choice:
|
||||
if tool_choice and (request_tools or not tools):
|
||||
request_data["tool_choice"] = tool_choice
|
||||
if tools:
|
||||
request_data["tools"] = tools
|
||||
if request_tools:
|
||||
request_data["tools"] = request_tools
|
||||
if top_k is not None:
|
||||
request_data["top_k"] = top_k
|
||||
if top_p is not None:
|
||||
|
|
@ -647,16 +669,15 @@ class LiteLLMMessagesToCompletionTransformationHandler:
|
|||
if "output_config" in extra_kwargs:
|
||||
request_data["output_config"] = extra_kwargs["output_config"]
|
||||
|
||||
custom_llm_provider: Final = extra_kwargs.get("custom_llm_provider")
|
||||
api_base: Final = extra_kwargs.get("api_base")
|
||||
(
|
||||
openai_request,
|
||||
tool_name_mapping,
|
||||
) = ANTHROPIC_ADAPTER.translate_completion_input_params_with_tool_mapping(
|
||||
request_data,
|
||||
custom_llm_provider=custom_llm_provider if isinstance(custom_llm_provider, str) else None,
|
||||
custom_llm_provider=target_provider,
|
||||
api_base=api_base if isinstance(api_base, str) else None,
|
||||
drop_params=drops_unsupported_params(extra_kwargs.get("drop_params")),
|
||||
drop_params=drop_params,
|
||||
)
|
||||
|
||||
if openai_request is None:
|
||||
|
|
@ -684,7 +705,11 @@ class LiteLLMMessagesToCompletionTransformationHandler:
|
|||
# Maintainability: when adding a new Anthropic-only request param to
|
||||
# ``AnthropicMessagesRequestOptionalParams``, also extend
|
||||
# ``ANTHROPIC_ONLY_REQUEST_KEYS`` here so it doesn't silently leak.
|
||||
excluded_keys: Final = ANTHROPIC_ONLY_REQUEST_KEYS | CONVERTED_STREAM_KEYS | {"anthropic_messages"}
|
||||
excluded_keys: Final = (
|
||||
ANTHROPIC_ONLY_REQUEST_KEYS
|
||||
| CONVERTED_STREAM_KEYS
|
||||
| ({"anthropic_messages", "mcp_servers"} if strips_mcp_servers else {"anthropic_messages"})
|
||||
)
|
||||
# NOTE: extra_kwargs was already coerced from None to {} at the top of
|
||||
# this method (line ~220). It is guaranteed to be a dict here.
|
||||
for key, value in extra_kwargs.items():
|
||||
|
|
|
|||
|
|
@ -493,10 +493,23 @@ class _CombinedChunkSplitter:
|
|||
for position, block in enumerate(blocks)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _without_sibling_finish(chunk: "ModelResponseStream") -> "ModelResponseStream":
|
||||
choices: Final = _optional_attr_sequence(chunk, "choices")
|
||||
if (
|
||||
len(choices) != 1
|
||||
or _optional_attr(choices[0], "index") in (None, 0)
|
||||
or _optional_attr(choices[0], "finish_reason") is None
|
||||
):
|
||||
return chunk
|
||||
unfinished: Final = copy.deepcopy(chunk)
|
||||
unfinished.choices[0].finish_reason = None
|
||||
return unfinished
|
||||
|
||||
def _expand(self, chunk: "ModelResponseStream") -> "tuple[ModelResponseStream, ...]":
|
||||
split_chunks: Final = tuple(
|
||||
split_chunk
|
||||
for combined_chunk in self._split(chunk)
|
||||
for combined_chunk in self._split(self._without_sibling_finish(chunk))
|
||||
for split_chunk in self._split_by_payload_kind(combined_chunk) # comprehension-ok: flattens nested splits
|
||||
)
|
||||
block_chunks: Final = tuple(itertools.chain.from_iterable(map(self._split_thinking_blocks, split_chunks)))
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ class _ClearToolUsesFields(BaseModel):
|
|||
clear_tool_inputs: bool | tuple[str, ...] | None = None
|
||||
|
||||
|
||||
_BLOCK: Final = TypeAdapter(Mapping[str, object])
|
||||
_BLOCKS: Final = TypeAdapter(tuple[Mapping[str, object], ...])
|
||||
# api.anthropic.com, measured 2026-10-19 and 2026-10-28: its wording for the shapes pydantic words differently
|
||||
_API_MESSAGES: Final = MappingProxyType(
|
||||
|
|
@ -317,12 +318,10 @@ def _clear_tool_results(
|
|||
new_blocks: list[object] = []
|
||||
mutated = False
|
||||
for block in content:
|
||||
if (
|
||||
isinstance(block, dict)
|
||||
and block.get("type") == "tool_result"
|
||||
and block.get("tool_use_id") in ids_to_clear
|
||||
):
|
||||
new_blocks.append(_cleared_result(block))
|
||||
if (parsed := _parse(_BLOCK, block)) is not None and _block_id(
|
||||
parsed, "tool_result", "tool_use_id"
|
||||
) in ids_to_clear:
|
||||
new_blocks.append(_cleared_result(parsed))
|
||||
mutated = True
|
||||
cleared += 1
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Final
|
||||
|
||||
from pydantic import JsonValue, TypeAdapter, ValidationError
|
||||
from typing_extensions import assert_never
|
||||
|
||||
from litellm.types.llms.anthropic import AnthropicMessagesTextParam
|
||||
from litellm.types.utils import CountContextManagement, TokenCountMessagesFormat
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class InferenceRequestView:
|
||||
messages: Sequence[Mapping[str, object]]
|
||||
system: object | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class InferenceRequestViewError:
|
||||
error_message: str
|
||||
status_code: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CountContextEdits:
|
||||
spec: JsonValue
|
||||
model: str
|
||||
tools: tuple[Mapping[str, object], ...] | None
|
||||
drop_invalid_edits: bool = False
|
||||
|
||||
|
||||
_CONTEXT_MANAGEMENT_SPEC: Final[TypeAdapter[Mapping[str, object] | tuple[Mapping[str, object], ...]]] = TypeAdapter(
|
||||
Mapping[str, object] | tuple[Mapping[str, object], ...]
|
||||
)
|
||||
_SYSTEM_BLOCKS: Final = TypeAdapter(tuple[AnthropicMessagesTextParam, ...])
|
||||
|
||||
|
||||
def count_context_edits(
|
||||
context_management: CountContextManagement | None,
|
||||
tools: Sequence[Mapping[str, object]] | None,
|
||||
litellm_params: Mapping[str, object],
|
||||
fallback_model: str,
|
||||
) -> CountContextEdits:
|
||||
model: Final = litellm_params.get("model")
|
||||
return CountContextEdits(
|
||||
spec=None if context_management is None else context_management.spec,
|
||||
model=model if isinstance(model, str) else fallback_model,
|
||||
tools=None if tools is None else tuple(tools),
|
||||
drop_invalid_edits=context_management is not None and context_management.drop_invalid_edits,
|
||||
)
|
||||
|
||||
|
||||
def _typed_spec(spec: JsonValue) -> Mapping[str, object] | tuple[Mapping[str, object], ...] | None:
|
||||
try:
|
||||
return _CONTEXT_MANAGEMENT_SPEC.validate_python(spec)
|
||||
except ValidationError:
|
||||
return None
|
||||
|
||||
|
||||
def _typed_system(system: object | None) -> str | tuple[AnthropicMessagesTextParam, ...] | None | ValidationError:
|
||||
if system is None or isinstance(system, str):
|
||||
return system
|
||||
try:
|
||||
return _SYSTEM_BLOCKS.validate_python(system)
|
||||
except ValidationError as error:
|
||||
return error
|
||||
|
||||
|
||||
def _view_without_edits(
|
||||
messages: Sequence[Mapping[str, object]],
|
||||
system: object | None,
|
||||
typed_system: str | Sequence[Mapping[str, object]] | None,
|
||||
) -> InferenceRequestView:
|
||||
from litellm.llms.anthropic.messages.context_management.editors.compact import (
|
||||
apply_client_compaction_block_history,
|
||||
)
|
||||
|
||||
history: Final = apply_client_compaction_block_history(
|
||||
messages=[dict(message) for message in messages],
|
||||
system=typed_system
|
||||
if typed_system is None or isinstance(typed_system, str)
|
||||
else [dict(block) for block in typed_system],
|
||||
)
|
||||
if history is None:
|
||||
return InferenceRequestView(messages=messages, system=system)
|
||||
return InferenceRequestView(messages=history.messages, system=history.system)
|
||||
|
||||
|
||||
async def _anthropic_inference_view(
|
||||
messages: Sequence[Mapping[str, object]], system: object | None, edits: CountContextEdits
|
||||
) -> InferenceRequestView | InferenceRequestViewError:
|
||||
from litellm.llms.anthropic.messages.adapters.handler import prepare_context_managed_request
|
||||
from litellm.llms.anthropic.messages.context_management import AnthropicContextManagementError
|
||||
from litellm.llms.anthropic.messages.native.utils import normalize_native_messages
|
||||
|
||||
typed_system: Final = _typed_system(system)
|
||||
if isinstance(typed_system, ValidationError):
|
||||
return InferenceRequestView(messages=messages, system=system)
|
||||
normalized: Final = normalize_native_messages(messages)
|
||||
spec: Final = _typed_spec(edits.spec)
|
||||
try:
|
||||
prepared: Final = await prepare_context_managed_request(
|
||||
model=edits.model,
|
||||
messages=normalized,
|
||||
tools=None if edits.tools is None else [dict(tool) for tool in edits.tools],
|
||||
system=[dict(block) for block in typed_system] if isinstance(typed_system, tuple) else typed_system,
|
||||
context_management_spec=(
|
||||
None if spec is None else dict(spec) if isinstance(spec, Mapping) else [dict(edit) for edit in spec]
|
||||
),
|
||||
litellm_metadata=None,
|
||||
additional_drop_params=None,
|
||||
llm_router=None,
|
||||
count_only=True,
|
||||
drop_invalid_edits=edits.drop_invalid_edits,
|
||||
)
|
||||
except AnthropicContextManagementError as error:
|
||||
if error.status_code >= 500:
|
||||
return _view_without_edits(normalized, system, typed_system)
|
||||
return InferenceRequestViewError(error_message=error.message, status_code=error.status_code)
|
||||
if prepared is None:
|
||||
return InferenceRequestView(messages=normalized, system=system)
|
||||
return InferenceRequestView(messages=prepared.messages, system=prepared.system)
|
||||
|
||||
|
||||
async def inference_request_view(
|
||||
messages: Sequence[Mapping[str, object]],
|
||||
system: object | None,
|
||||
messages_format: TokenCountMessagesFormat,
|
||||
edits: CountContextEdits,
|
||||
) -> InferenceRequestView | InferenceRequestViewError:
|
||||
"""Apply the history rewrites and context edits /v1/messages runs before it converts a body for another provider."""
|
||||
match messages_format:
|
||||
case "openai":
|
||||
return InferenceRequestView(messages=messages, system=system)
|
||||
case "anthropic":
|
||||
return await _anthropic_inference_view(messages, system, edits)
|
||||
case _:
|
||||
assert_never(messages_format)
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
import asyncio
|
||||
import contextvars
|
||||
from collections.abc import AsyncIterator, Awaitable, Callable, Coroutine, Iterator, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import Final, cast
|
||||
|
||||
|
|
@ -15,6 +16,7 @@ from pydantic import TypeAdapter
|
|||
|
||||
import litellm
|
||||
from litellm.anthropic_beta_headers_manager import aload_beta_headers_config
|
||||
from litellm.constants import PROVIDERS_THAT_AUTHENTICATE_ON_PROVIDER_INFO
|
||||
from litellm.integrations.anthropic_cache_control_hook import AnthropicCacheControlHook
|
||||
from litellm.litellm_core_utils.core_helpers import as_str_mapping
|
||||
from litellm.litellm_core_utils.exception_mapping_utils import exception_type
|
||||
|
|
@ -44,7 +46,7 @@ from litellm.types.llms.anthropic_messages.anthropic_response import (
|
|||
AnthropicMessagesResponse,
|
||||
)
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
from litellm.types.utils import CallTypes, LlmProviders
|
||||
from litellm.types.utils import CallTypes, LlmProviders, LlmProvidersSet
|
||||
from litellm.utils import ProviderConfigManager, client
|
||||
|
||||
from ..adapters.handler import LiteLLMMessagesToCompletionTransformationHandler, request_safeguards_evaluator
|
||||
|
|
@ -94,6 +96,68 @@ def _responses_mode_is_lost_by_prefix_strip(
|
|||
) and not _bridges_to_responses_api(resolved_model, custom_llm_provider, deployment_model_info)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _BridgeTarget:
|
||||
model: str
|
||||
api_key: str | None
|
||||
api_base: str | None
|
||||
|
||||
|
||||
def _resolve_requested_provider(
|
||||
model: str, custom_llm_provider: str | None, litellm_params: GenericLiteLLMParams
|
||||
) -> tuple[str, str, str | None, str | None]:
|
||||
resolved: Final = litellm.get_llm_provider(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
api_base=litellm_params.api_base,
|
||||
api_key=litellm_params.api_key,
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
requested_provider: Final = model.partition("/")[0]
|
||||
if (
|
||||
custom_llm_provider in (None, requested_provider)
|
||||
or requested_provider not in LlmProvidersSet
|
||||
or requested_provider in PROVIDERS_THAT_AUTHENTICATE_ON_PROVIDER_INFO
|
||||
):
|
||||
return resolved
|
||||
own: Final = _resolve_by_model_prefix(model, litellm_params)
|
||||
return own if own is not None and own[1] == custom_llm_provider else resolved
|
||||
|
||||
|
||||
def _resolve_by_model_prefix(
|
||||
model: str, litellm_params: GenericLiteLLMParams
|
||||
) -> tuple[str, str, str | None, str | None] | None:
|
||||
try:
|
||||
return litellm.get_llm_provider(
|
||||
model=model,
|
||||
api_base=litellm_params.api_base,
|
||||
api_key=litellm_params.api_key,
|
||||
litellm_params=litellm_params.model_copy(update={"custom_llm_provider": None}),
|
||||
)
|
||||
except litellm.BadRequestError:
|
||||
return None
|
||||
|
||||
|
||||
def _sets_own_entra_credential(litellm_params: GenericLiteLLMParams) -> bool:
|
||||
return bool(
|
||||
litellm_params.azure_ad_token
|
||||
or (litellm_params.model_extra or {}).get("azure_ad_token_provider") is not None
|
||||
or (litellm_params.tenant_id and litellm_params.client_id and litellm_params.client_secret)
|
||||
or (litellm_params.azure_username and litellm_params.azure_password and litellm_params.client_id)
|
||||
)
|
||||
|
||||
|
||||
def _bridge_target(requested: _BridgeTarget, resolved: _BridgeTarget, custom_llm_provider: str) -> _BridgeTarget:
|
||||
requested_provider: Final = requested.model.partition("/")[0]
|
||||
if requested_provider == custom_llm_provider or requested_provider not in LlmProvidersSet:
|
||||
return requested
|
||||
return _BridgeTarget(
|
||||
model=f"{custom_llm_provider}/{resolved.model}",
|
||||
api_key=requested.api_key if requested.api_base else requested.api_key or resolved.api_key,
|
||||
api_base=resolved.api_base,
|
||||
)
|
||||
|
||||
|
||||
def _should_route_to_responses_api(
|
||||
custom_llm_provider: str | None,
|
||||
requested_model: str | None = None,
|
||||
|
|
@ -624,13 +688,7 @@ def anthropic_messages_handler(
|
|||
custom_llm_provider,
|
||||
dynamic_api_key,
|
||||
dynamic_api_base,
|
||||
) = litellm.get_llm_provider(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
api_base=litellm_params.api_base,
|
||||
api_key=litellm_params.api_key,
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
) = _resolve_requested_provider(model, custom_llm_provider, litellm_params)
|
||||
|
||||
# Store agentic loop params in logging object for agentic hooks
|
||||
# This provides original request context needed for follow-up calls
|
||||
|
|
@ -656,6 +714,16 @@ def anthropic_messages_handler(
|
|||
mock_response=litellm_params.mock_response,
|
||||
)
|
||||
|
||||
bridge: Final = _bridge_target(
|
||||
_BridgeTarget(model=original_model, api_key=api_key, api_base=api_base),
|
||||
_BridgeTarget(
|
||||
model=model,
|
||||
api_key=None if _sets_own_entra_credential(litellm_params) else dynamic_api_key,
|
||||
api_base=dynamic_api_base,
|
||||
),
|
||||
custom_llm_provider,
|
||||
)
|
||||
|
||||
# Expand litellm_proxy MCP references through the MCP gateway before dispatch, so every
|
||||
# downstream path (native passthrough and both bridges) gets real tools rather than a
|
||||
# reference the provider cannot resolve. Popped from kwargs so it never reaches the provider.
|
||||
|
|
@ -682,8 +750,8 @@ def anthropic_messages_handler(
|
|||
top_k=top_k,
|
||||
top_p=top_p,
|
||||
container=container,
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
api_key=bridge.api_key,
|
||||
api_base=bridge.api_base,
|
||||
client=client,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
**kwargs,
|
||||
|
|
@ -700,7 +768,7 @@ def anthropic_messages_handler(
|
|||
return LiteLLMMessagesToResponsesAPIHandler.anthropic_messages_handler(
|
||||
max_tokens=max_tokens,
|
||||
messages=messages,
|
||||
model=original_model,
|
||||
model=bridge.model,
|
||||
metadata=metadata,
|
||||
stop_sequences=stop_sequences,
|
||||
stream=stream,
|
||||
|
|
@ -716,8 +784,8 @@ def anthropic_messages_handler(
|
|||
top_k=top_k,
|
||||
top_p=top_p,
|
||||
_is_async=is_async,
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
api_key=bridge.api_key,
|
||||
api_base=bridge.api_base,
|
||||
client=client,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
**kwargs,
|
||||
|
|
@ -730,7 +798,7 @@ def anthropic_messages_handler(
|
|||
return LiteLLMMessagesToCompletionTransformationHandler.anthropic_messages_handler(
|
||||
max_tokens=max_tokens,
|
||||
messages=messages,
|
||||
model=original_model,
|
||||
model=bridge.model,
|
||||
metadata=metadata,
|
||||
stop_sequences=stop_sequences,
|
||||
stream=stream,
|
||||
|
|
@ -744,8 +812,8 @@ def anthropic_messages_handler(
|
|||
top_k=top_k,
|
||||
top_p=top_p,
|
||||
_is_async=is_async,
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
api_key=bridge.api_key,
|
||||
api_base=bridge.api_base,
|
||||
client=client,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
**kwargs,
|
||||
|
|
|
|||
|
|
@ -21,9 +21,10 @@ from litellm.litellm_core_utils.asyncify import (
|
|||
from litellm.litellm_core_utils.core_helpers import as_str_mapping, drops_unsupported_params
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
cache_control_as_prompt_cache_breakpoint,
|
||||
prompt_cache_breakpoint_for_wire,
|
||||
prompt_cache_breakpoint_with_readable_mode,
|
||||
with_prompt_cache_breakpoint,
|
||||
)
|
||||
from litellm.litellm_core_utils.tool_fields import drops_mcp_servers, tools_without_mcp_servers
|
||||
from litellm.responses.utils import auto_reasoning_summary
|
||||
from litellm.types.integrations.custom_logger import CONVERTED_STREAM_KEYS
|
||||
from litellm.types.llms.anthropic import (
|
||||
|
|
@ -207,7 +208,7 @@ def _without_prompt_cache_breakpoint(block: Mapping[str, object]) -> Mapping[str
|
|||
def _cache_control_as_wire_safe_prompt_cache_breakpoint(block: Mapping[str, object]) -> Mapping[str, object]:
|
||||
if "prompt_cache_breakpoint" not in block:
|
||||
return cache_control_as_prompt_cache_breakpoint(block)
|
||||
wire_marker: Final = prompt_cache_breakpoint_for_wire(block["prompt_cache_breakpoint"], drop_params=True)
|
||||
wire_marker: Final = prompt_cache_breakpoint_with_readable_mode(block["prompt_cache_breakpoint"], drop_params=True)
|
||||
return cache_control_as_prompt_cache_breakpoint(
|
||||
with_prompt_cache_breakpoint(_without_prompt_cache_breakpoint(block), wire_marker)
|
||||
)
|
||||
|
|
@ -272,6 +273,14 @@ def _build_responses_kwargs(
|
|||
"""
|
||||
forwarded_kwargs: Final = _forwarded_kwargs(extra_kwargs)
|
||||
reject_malformed_tool_result_text(messages, model, forwarded_kwargs)
|
||||
target_provider: Final = forwarded_kwargs.get("custom_llm_provider")
|
||||
request_tools: Final = tools_without_mcp_servers(
|
||||
forwarded_kwargs.get("mcp_servers"),
|
||||
tools or (),
|
||||
model=model,
|
||||
route=target_provider if isinstance(target_provider, str) else "this route",
|
||||
drop_params=drops_mcp_servers(forwarded_kwargs),
|
||||
)
|
||||
block_rewrite: Final = _prompt_cache_block_rewrite(model, forwarded_kwargs)
|
||||
request_messages: Final[AnthropicRequestMessages] = (
|
||||
[
|
||||
|
|
@ -304,10 +313,10 @@ def _build_responses_kwargs(
|
|||
request_data["temperature"] = temperature
|
||||
if thinking:
|
||||
request_data["thinking"] = thinking
|
||||
if tool_choice:
|
||||
if tool_choice and (request_tools or not tools):
|
||||
request_data["tool_choice"] = tool_choice
|
||||
if tools:
|
||||
request_data["tools"] = tools
|
||||
if request_tools:
|
||||
request_data["tools"] = list(request_tools)
|
||||
if top_p is not None:
|
||||
request_data["top_p"] = top_p
|
||||
if output_format:
|
||||
|
|
@ -347,7 +356,7 @@ def _build_responses_kwargs(
|
|||
responses_kwargs["stream"] = True
|
||||
|
||||
# Forward litellm-specific kwargs (api_key, api_base, logging obj, etc.)
|
||||
excluded: Final = CONVERTED_STREAM_KEYS | {"anthropic_messages"}
|
||||
excluded: Final = CONVERTED_STREAM_KEYS | {"anthropic_messages", "mcp_servers"}
|
||||
for key, value in forwarded_kwargs.items():
|
||||
if key == "litellm_logging_obj" and value is not None:
|
||||
from litellm.litellm_core_utils.litellm_logging import (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""Support for Azure OpenAI gpt-5 model family."""
|
||||
|
||||
from collections.abc import Collection
|
||||
from collections.abc import Collection, Mapping
|
||||
from typing import ClassVar, Final
|
||||
|
||||
import litellm
|
||||
|
|
@ -105,7 +105,7 @@ class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config):
|
|||
model: str,
|
||||
messages: list[AllMessageValues],
|
||||
optional_params: dict,
|
||||
litellm_params: dict,
|
||||
litellm_params: Mapping[str, object],
|
||||
headers: dict,
|
||||
) -> dict:
|
||||
model = model.replace(self.GPT5_SERIES_ROUTE, "")
|
||||
|
|
|
|||
|
|
@ -5,12 +5,16 @@ from typing import TYPE_CHECKING, Any, Final
|
|||
from httpx._models import Headers, Response
|
||||
|
||||
import litellm
|
||||
from litellm.integrations.anthropic_cache_control_hook import deployment_prompt_cache_breakpoint_support
|
||||
from litellm.litellm_core_utils.core_helpers import drops_unsupported_params
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
chat_completion_prompt_cache_breakpoints_for_wire,
|
||||
drop_tool_reference_parts_from_tool_messages,
|
||||
flatten_combinators_and_drop_non_python_regex_patterns,
|
||||
hoist_images_from_tool_messages,
|
||||
system_messages_first,
|
||||
tool_with_sanitized_parameters,
|
||||
without_prompt_cache_breakpoints,
|
||||
)
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
convert_to_azure_openai_messages,
|
||||
|
|
@ -290,11 +294,16 @@ class AzureOpenAIConfig(BaseConfig):
|
|||
model: str,
|
||||
messages: list[AllMessageValues],
|
||||
optional_params: dict,
|
||||
litellm_params: dict,
|
||||
litellm_params: Mapping[str, object],
|
||||
headers: dict,
|
||||
) -> dict:
|
||||
ordered_messages: Final = system_messages_first(messages) if litellm.openai_system_messages_first else messages
|
||||
stripped_messages: Final = drop_tool_reference_parts_from_tool_messages(ordered_messages)
|
||||
wire_messages: Final = (
|
||||
self._with_wire_safe_prompt_cache_breakpoints(model, ordered_messages, litellm_params)
|
||||
if drops_unsupported_params(litellm_params.get("drop_params"))
|
||||
else ordered_messages
|
||||
)
|
||||
stripped_messages: Final = drop_tool_reference_parts_from_tool_messages(wire_messages)
|
||||
azure_messages: Final = convert_to_azure_openai_messages(hoist_images_from_tool_messages(stripped_messages))
|
||||
request_params: Final = MappingProxyType(
|
||||
{
|
||||
|
|
@ -310,6 +319,14 @@ class AzureOpenAIConfig(BaseConfig):
|
|||
**sanitized_tools_update(optional_params),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _with_wire_safe_prompt_cache_breakpoints(
|
||||
model: str, messages: list[AllMessageValues], litellm_params: Mapping[str, object]
|
||||
) -> list[AllMessageValues]:
|
||||
if deployment_prompt_cache_breakpoint_support(model, "azure", litellm_params) is False:
|
||||
return [without_prompt_cache_breakpoints(message) for message in messages]
|
||||
return [chat_completion_prompt_cache_breakpoints_for_wire(message, drop_params=True) for message in messages]
|
||||
|
||||
def transform_response(
|
||||
self,
|
||||
model: str,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import re
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Callable, Mapping
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal
|
||||
from urllib.parse import urlsplit
|
||||
|
|
@ -11,6 +12,7 @@ from pydantic import TypeAdapter
|
|||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.constants import AZURE_DEFAULT_RESPONSES_API_VERSION
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import prompt_cache_breakpoint_within
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
|
||||
from litellm.llms.azure.chat.gpt_reasoning_transformation import AzureOpenAIGPT5Config
|
||||
from litellm.llms.azure.common_utils import BaseAzureLLM
|
||||
|
|
@ -29,6 +31,7 @@ else:
|
|||
LiteLLMLoggingObj = Any
|
||||
|
||||
_DECODED_JSON: Final = TypeAdapter(object)
|
||||
_AZURE_RESPONSES_PROMPT_CACHE_BREAKPOINT: Final = TypeAdapter(AzureResponsesPromptCacheBreakpoint)
|
||||
|
||||
|
||||
_DATED_GA_API_VERSION: Final = re.compile(r"\d{4}-\d{2}-\d{2}")
|
||||
|
|
@ -81,6 +84,13 @@ class AzureOpenAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
|||
def _is_gpt_5_model(model: str) -> bool:
|
||||
return AzureOpenAIGPT5Config.is_model_gpt_5_model(model)
|
||||
|
||||
def _prompt_cache_breakpoint_to_wire(
|
||||
self, litellm_params: GenericLiteLLMParams, reads_breakpoints: bool, known_support: bool | None
|
||||
) -> Callable[[object], object]:
|
||||
if reads_breakpoints or known_support is True:
|
||||
return partial(prompt_cache_breakpoint_within, _AZURE_RESPONSES_PROMPT_CACHE_BREAKPOINT)
|
||||
return super()._prompt_cache_breakpoint_to_wire(litellm_params, reads_breakpoints, known_support)
|
||||
|
||||
@staticmethod
|
||||
def _supports_reasoning_effort_none(model: str) -> bool:
|
||||
return AzureOpenAIGPT5Config.supports_reasoning_effort_level(model, "none")
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from pydantic import JsonValue
|
|||
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.anthropic_beta_headers_manager import aload_beta_headers_config
|
||||
from litellm.litellm_core_utils.asyncify import asyncify
|
||||
from litellm.litellm_core_utils.core_helpers import drops_unsupported_params
|
||||
from litellm.llms.anthropic.common_utils import AnthropicError
|
||||
|
|
@ -39,6 +40,7 @@ class AzureAIAnthropicCountTokensHandler(AzureAIAnthropicCountTokensConfig):
|
|||
tools: list[dict[str, JsonValue]] | None = None,
|
||||
system: object = None,
|
||||
optional_params: Mapping[str, JsonValue] | None = None,
|
||||
context_management: JsonValue = None,
|
||||
) -> dict[str, object]:
|
||||
"""
|
||||
Handle a CountTokens request using httpx with Azure authentication.
|
||||
|
|
@ -70,6 +72,7 @@ class AzureAIAnthropicCountTokensHandler(AzureAIAnthropicCountTokensConfig):
|
|||
tools=tools,
|
||||
system=system,
|
||||
optional_params=optional_params,
|
||||
context_management=context_management,
|
||||
drop_params=drops_unsupported_params((litellm_params or {}).get("drop_params")),
|
||||
)
|
||||
|
||||
|
|
@ -80,11 +83,14 @@ class AzureAIAnthropicCountTokensHandler(AzureAIAnthropicCountTokensConfig):
|
|||
|
||||
verbose_logger.debug("Making request to: %s", endpoint_url)
|
||||
|
||||
if context_management is not None:
|
||||
await aload_beta_headers_config()
|
||||
# Get required headers with Azure authentication
|
||||
headers: Final = self.get_required_headers(
|
||||
api_key=api_key,
|
||||
litellm_params=litellm_params,
|
||||
messages=messages,
|
||||
context_management=context_management,
|
||||
)
|
||||
if not {"x-api-key", "Authorization"} & headers.keys():
|
||||
raise AnthropicError(
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ class AzureAIAnthropicTokenCounter(BaseTokenCounter):
|
|||
tools=tools,
|
||||
system=system,
|
||||
optional_params=count_options,
|
||||
context_management=None if context_management is None else context_management.spec,
|
||||
)
|
||||
|
||||
if result is not None:
|
||||
|
|
|
|||
|
|
@ -8,9 +8,16 @@ from collections.abc import Sequence
|
|||
from typing import Final
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from pydantic import JsonValue
|
||||
|
||||
from litellm.anthropic_beta_headers_manager import filter_and_transform_beta_headers
|
||||
from litellm.constants import ANTHROPIC_TOKEN_COUNTING_BETA_VERSION
|
||||
from litellm.litellm_core_utils.url_utils import join_api_base
|
||||
from litellm.llms.anthropic.common_utils import count_tokens_feature_betas, merge_anthropic_beta_headers
|
||||
from litellm.llms.anthropic.common_utils import (
|
||||
context_management_betas,
|
||||
count_tokens_feature_betas,
|
||||
merge_anthropic_beta_headers,
|
||||
)
|
||||
from litellm.llms.anthropic.count_tokens.transformation import (
|
||||
AnthropicCountTokensConfig,
|
||||
)
|
||||
|
|
@ -31,6 +38,7 @@ class AzureAIAnthropicCountTokensConfig(AnthropicCountTokensConfig):
|
|||
api_key: str | None,
|
||||
litellm_params: dict[str, object] | None = None,
|
||||
messages: Sequence[object] = (),
|
||||
context_management: JsonValue = None,
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Get the required headers for the Azure AI Anthropic CountTokens API.
|
||||
|
|
@ -42,6 +50,7 @@ class AzureAIAnthropicCountTokensConfig(AnthropicCountTokensConfig):
|
|||
api_key: The Azure AI API key
|
||||
litellm_params: Optional LiteLLM parameters for additional auth config
|
||||
messages: The request messages, read for betas the counted features need
|
||||
context_management: The edit spec the count forwards, read for the betas its edits need
|
||||
|
||||
Returns:
|
||||
Dictionary of required headers with both x-api-key and Azure authentication
|
||||
|
|
@ -51,7 +60,11 @@ class AzureAIAnthropicCountTokensConfig(AnthropicCountTokensConfig):
|
|||
"Content-Type": "application/json",
|
||||
"anthropic-version": "2023-06-01",
|
||||
"anthropic-beta": merge_anthropic_beta_headers(
|
||||
ANTHROPIC_TOKEN_COUNTING_BETA_VERSION, count_tokens_feature_betas("azure_ai", {}, messages)
|
||||
ANTHROPIC_TOKEN_COUNTING_BETA_VERSION,
|
||||
(
|
||||
*count_tokens_feature_betas("azure_ai", {}, messages),
|
||||
*filter_and_transform_beta_headers(list(context_management_betas(context_management)), "azure_ai"),
|
||||
),
|
||||
),
|
||||
**({"x-api-key": api_key} if api_key else {}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ def responses_batch_body_to_chat_body(
|
|||
openai_request_body: Mapping[str, object],
|
||||
custom_llm_provider: str | None = None,
|
||||
drop_params: bool | None = None,
|
||||
*,
|
||||
caller_input: str | ResponseInputParam | None = None,
|
||||
) -> dict[str, object]: # mutable-ok: provider transforms take the bridged chat body as a plain dict
|
||||
"""
|
||||
Rewrite the body of an OpenAI `/v1/responses` batch record as a Chat Completions body.
|
||||
|
|
@ -52,5 +54,6 @@ def responses_batch_body_to_chat_body(
|
|||
responses_api_request=responses_request,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
drop_params=drop_params,
|
||||
caller_input=caller_input,
|
||||
metadata=openai_request_body.get("metadata"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ from litellm.litellm_core_utils.prompt_templates.mid_conversation_system import
|
|||
parts_of,
|
||||
)
|
||||
from litellm.litellm_core_utils.reasoning_effort_utils import OPAQUE_VALUE, warn_dropped_reasoning
|
||||
from litellm.litellm_core_utils.tool_fields import tool_params_without_hosted_mcp
|
||||
from litellm.llms.anthropic.chat.transformation import (
|
||||
DROP_UNSUPPORTED_ADAPTIVE_THINKING_WARNING,
|
||||
DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING,
|
||||
|
|
@ -1068,20 +1069,23 @@ class AmazonConverseConfig(BaseConfig):
|
|||
drop_params: bool,
|
||||
model_info: Mapping[str, object] | None = None,
|
||||
) -> dict:
|
||||
is_thinking_enabled: Final = self.is_thinking_enabled(non_default_params)
|
||||
request_params: Final = tool_params_without_hosted_mcp(
|
||||
non_default_params, model=model, route="bedrock", drop_params=drop_params
|
||||
)
|
||||
is_thinking_enabled: Final = self.is_thinking_enabled(request_params)
|
||||
base_model: Final = BedrockModelInfo.get_base_model(model)
|
||||
drop_thinking_param: Final = self._is_deepseek_model(model=model, base_model=base_model)
|
||||
drop_reasoning_effort_param: Final = self._model_rejects_reasoning_effort_param(
|
||||
model=model, base_model=base_model
|
||||
)
|
||||
|
||||
for param, value in non_default_params.items():
|
||||
for param, value in request_params.items():
|
||||
if param == "response_format" and isinstance(value, dict):
|
||||
optional_params = self._translate_response_format_param(
|
||||
value=value,
|
||||
model=model,
|
||||
optional_params=optional_params,
|
||||
non_default_params=non_default_params,
|
||||
non_default_params=request_params,
|
||||
is_thinking_enabled=is_thinking_enabled,
|
||||
model_info=model_info,
|
||||
)
|
||||
|
|
@ -1135,7 +1139,7 @@ class AmazonConverseConfig(BaseConfig):
|
|||
),
|
||||
),
|
||||
model=model,
|
||||
non_default_params=non_default_params,
|
||||
non_default_params=request_params,
|
||||
optional_params=optional_params,
|
||||
)
|
||||
if param == "tool_choice":
|
||||
|
|
@ -1160,7 +1164,7 @@ class AmazonConverseConfig(BaseConfig):
|
|||
and value.get("type") == "adaptive"
|
||||
and not AnthropicConfig.is_adaptive_thinking_model(model, "bedrock")
|
||||
):
|
||||
max_tokens = non_default_params.get("max_completion_tokens") or non_default_params.get("max_tokens")
|
||||
max_tokens = request_params.get("max_completion_tokens") or request_params.get("max_tokens")
|
||||
legacy_thinking = AnthropicConfig.map_reasoning_effort(
|
||||
reasoning_effort="medium",
|
||||
model=model,
|
||||
|
|
@ -1230,7 +1234,7 @@ class AmazonConverseConfig(BaseConfig):
|
|||
# Nova 2 handles token budgeting differently through reasoningConfig
|
||||
if "gpt-oss" not in model and not self._is_nova_2_model(model):
|
||||
self.update_optional_params_with_thinking_tokens(
|
||||
non_default_params=non_default_params, optional_params=optional_params
|
||||
non_default_params=request_params, optional_params=optional_params
|
||||
)
|
||||
|
||||
final_is_thinking_enabled: Final = self.is_thinking_enabled(optional_params)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
from collections.abc import Mapping
|
||||
from collections.abc import Callable, Mapping
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
import httpx
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
import litellm
|
||||
from litellm.anthropic_beta_gated_fields import strip_beta_gated_request_body
|
||||
from litellm.anthropic_beta_headers_manager import filter_and_transform_beta_headers
|
||||
from litellm.constants import DEFAULT_ANTHROPIC_CHAT_MAX_TOKENS
|
||||
from litellm.litellm_core_utils.llm_cost_calc.guardrail_cost import object_mapping
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
convert_to_anthropic_image_obj,
|
||||
|
|
@ -25,7 +27,9 @@ from litellm.llms.bedrock.chat.invoke_transformations.base_invoke_transformation
|
|||
)
|
||||
from litellm.llms.bedrock.common_utils import (
|
||||
BEDROCK_INVOKE_TRACE_FIELD,
|
||||
BedrockModelInfo,
|
||||
apply_bedrock_invoke_structured_output,
|
||||
bedrock_capability_model,
|
||||
bedrock_chat_uses_native_structured_output,
|
||||
bedrock_cleaned_output_schema,
|
||||
bedrock_supports_tool_search,
|
||||
|
|
@ -35,6 +39,7 @@ from litellm.llms.bedrock.common_utils import (
|
|||
normalize_custom_field_on_tools,
|
||||
normalize_tool_input_schema_types_for_bedrock_invoke,
|
||||
requests_opt_in_native_output,
|
||||
strip_bedrock_routing_prefix,
|
||||
strip_unsupported_bedrock_invoke_output_config_keys,
|
||||
tools_without_eager_input_streaming,
|
||||
)
|
||||
|
|
@ -59,6 +64,19 @@ _NON_DEFAULT_PARAMS: Final = TypeAdapter(Mapping[str, object])
|
|||
_LITELLM_PARAMS: Final = TypeAdapter(Mapping[str, object])
|
||||
|
||||
|
||||
def _anthropic_config_max_tokens() -> int | None:
|
||||
return litellm.AnthropicConfig.max_tokens
|
||||
|
||||
|
||||
def _mapped_max_tokens(model: str) -> int | None:
|
||||
row: Final = object_mapping(litellm.model_cost.get(model)) or {} # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] # cost map rows are untyped JSON
|
||||
match row.get("max_output_tokens"):
|
||||
case int() as max_tokens:
|
||||
return max_tokens
|
||||
case _:
|
||||
return None
|
||||
|
||||
|
||||
class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
|
||||
"""
|
||||
Reference:
|
||||
|
|
@ -79,9 +97,18 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
|
|||
|
||||
anthropic_version: str = "bedrock-2023-05-31"
|
||||
|
||||
def __init__(self, model_info: Mapping[str, object] | None = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
model_info: Mapping[str, object] | None = None,
|
||||
read_configured_max_tokens: Callable[[], int | None] = _anthropic_config_max_tokens,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._deployment_model_info: Final = model_info
|
||||
self._read_configured_max_tokens: Final = read_configured_max_tokens
|
||||
|
||||
@classmethod
|
||||
def get_config(cls, *, model: str | None = None) -> dict[str, object]: # mutable-ok: overrides BaseConfig's dict
|
||||
return {key: value for key, value in super().get_config(model=model).items() if key != "max_tokens"}
|
||||
|
||||
@property
|
||||
def custom_llm_provider(self) -> str | None:
|
||||
|
|
@ -241,6 +268,36 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
|
|||
headers=headers,
|
||||
)
|
||||
|
||||
def _default_max_tokens_models(self, model: str, litellm_params: Mapping[str, object]) -> tuple[str, ...]:
|
||||
return (
|
||||
BedrockModelInfo.get_base_model(
|
||||
bedrock_capability_model(strip_bedrock_routing_prefix(model), litellm_params.get("base_model"))
|
||||
),
|
||||
)
|
||||
|
||||
def _default_max_tokens(self, model: str, litellm_params: Mapping[str, object]) -> int:
|
||||
configured_max_tokens: Final = self._read_configured_max_tokens()
|
||||
match deployment_model_info(litellm_params).get("max_output_tokens"):
|
||||
case int() as deployment_max_tokens:
|
||||
return (
|
||||
deployment_max_tokens
|
||||
if configured_max_tokens is None
|
||||
else min(deployment_max_tokens, configured_max_tokens)
|
||||
)
|
||||
case _ if configured_max_tokens is not None:
|
||||
return configured_max_tokens
|
||||
case _:
|
||||
return next(
|
||||
(
|
||||
max_tokens
|
||||
for max_tokens in map(
|
||||
_mapped_max_tokens, self._default_max_tokens_models(model, litellm_params)
|
||||
)
|
||||
if max_tokens is not None
|
||||
),
|
||||
DEFAULT_ANTHROPIC_CHAT_MAX_TOKENS,
|
||||
)
|
||||
|
||||
def _build_bedrock_anthropic_request_base(
|
||||
self,
|
||||
model: str,
|
||||
|
|
@ -257,13 +314,21 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
|
|||
model=model,
|
||||
output_config=filtered_params["output_config"],
|
||||
)
|
||||
filtered_params = self._normalize_bedrock_tool_search_tools(filtered_params)
|
||||
normalized_params: Final = self._normalize_bedrock_tool_search_tools(filtered_params)
|
||||
default_max_tokens: Final = (
|
||||
None
|
||||
if "max_tokens" in normalized_params
|
||||
else self._default_max_tokens(model, _LITELLM_PARAMS.validate_python(litellm_params))
|
||||
)
|
||||
request_params: Final = (
|
||||
normalized_params if default_max_tokens is None else {**normalized_params, "max_tokens": default_max_tokens}
|
||||
)
|
||||
|
||||
anthropic_request: Final = AnthropicConfig.transform_request(
|
||||
self,
|
||||
model=model,
|
||||
messages=messages,
|
||||
optional_params=filtered_params,
|
||||
optional_params=request_params,
|
||||
litellm_params=litellm_params,
|
||||
headers=headers,
|
||||
)
|
||||
|
|
@ -377,7 +442,7 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
|
|||
"data": image_chunk["data"],
|
||||
}
|
||||
|
||||
def _normalize_bedrock_tool_search_tools(self, optional_params: dict) -> dict:
|
||||
def _normalize_bedrock_tool_search_tools(self, optional_params: dict) -> dict[str, object]:
|
||||
"""
|
||||
Convert tool search entries to the format supported by the Bedrock Invoke API.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ class AmazonMantleConfig(AmazonAnthropicClaudeConfig):
|
|||
region=region,
|
||||
)
|
||||
|
||||
def _default_max_tokens_models(self, model: str, litellm_params: Mapping[str, object]) -> tuple[str, ...]:
|
||||
return (f"bedrock_mantle/{model}", *super()._default_max_tokens_models(model, litellm_params))
|
||||
|
||||
def validate_environment(
|
||||
self,
|
||||
headers: dict,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ class BedrockClaudePlatformConfig(BedrockClaudePlatformMixin, AnthropicConfig):
|
|||
def should_strip_billing_metadata(self) -> bool:
|
||||
return True
|
||||
|
||||
def runs_hosted_mcp(self) -> bool:
|
||||
return True
|
||||
|
||||
def validate_environment(
|
||||
self,
|
||||
headers: dict,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ from litellm.llms.base_llm.base_utils import BaseLLMModelInfo, BaseTokenCounter
|
|||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
from litellm.llms.bedrock.request_metadata import bedrock_request_metadata_is_owned
|
||||
from litellm.secret_managers.main import get_secret, get_secret_str
|
||||
from litellm.types.llms.anthropic import ANTHROPIC_BETA_HEADER_VALUES
|
||||
from litellm.types.llms.bedrock import AWS_AUTH_PARAM_KEYS, AwsAuthParams
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -50,6 +51,12 @@ if TYPE_CHECKING:
|
|||
from litellm.types.llms.openai import AllMessageValues
|
||||
|
||||
BEDROCK_INVOKE_TRACE_FIELD: Final = "amazon-bedrock-trace"
|
||||
BEDROCK_INVOKE_CONTEXT_MANAGEMENT_EDIT_BETAS: Final[Mapping[str, str]] = MappingProxyType(
|
||||
{
|
||||
"compact_20260112": ANTHROPIC_BETA_HEADER_VALUES.COMPACT_2026_01_12.value,
|
||||
"clear_tool_uses_20250919": ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
_ERROR_REQUEST_URL: Final = "https://docs.litellm.ai/docs"
|
||||
|
|
|
|||
|
|
@ -13,9 +13,14 @@ from typing_extensions import assert_never
|
|||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
|
||||
from litellm.llms.anthropic.common_utils import context_management_betas, context_management_with_edits
|
||||
from litellm.llms.anthropic.count_tokens.transformation import ChatCountTranslationError, anthropic_count_fields
|
||||
from litellm.llms.base_llm.base_utils import BaseTokenCounter
|
||||
from litellm.llms.bedrock.common_utils import BedrockError, get_bedrock_base_model
|
||||
from litellm.llms.bedrock.common_utils import (
|
||||
BEDROCK_INVOKE_CONTEXT_MANAGEMENT_EDIT_BETAS,
|
||||
BedrockError,
|
||||
get_bedrock_base_model,
|
||||
)
|
||||
from litellm.llms.bedrock.count_tokens.handler import BedrockCountTokensHandler
|
||||
from litellm.llms.bedrock.count_tokens.mantle_handler import BedrockMantleCountTokensHandler
|
||||
from litellm.llms.bedrock_mantle.common_utils import is_mantle_claude_model
|
||||
|
|
@ -74,6 +79,16 @@ def _runtime_rejected_claude_model(outcome: CountTokensOutcome, resolved_model:
|
|||
)
|
||||
|
||||
|
||||
def _mantle_retry_outcome(
|
||||
runtime: CountTokensOutcome, mantle: CountTokensOutcome, carried_edits: bool
|
||||
) -> CountTokensOutcome:
|
||||
match runtime, mantle:
|
||||
case CountTokensFailure(status_code=400), CountTokensFailure(status_code=404) if carried_edits:
|
||||
return runtime
|
||||
case _:
|
||||
return mantle
|
||||
|
||||
|
||||
def _request_data(
|
||||
model: str, messages: Sequence[Mapping[str, object]], tools: Sequence[Mapping[str, object]] | None, system: object
|
||||
) -> Mapping[str, object]:
|
||||
|
|
@ -85,6 +100,33 @@ def _request_data(
|
|||
}
|
||||
|
||||
|
||||
def _invoke_model_message(message: Mapping[str, object]) -> Mapping[str, object]:
|
||||
content: Final = message.get("content")
|
||||
return (
|
||||
{**message, "content": [{"type": "text", "text": content}]} if isinstance(content, str) and content else message
|
||||
)
|
||||
|
||||
|
||||
def _invoke_supports_edit(edit: JsonValue) -> bool:
|
||||
edit_type: Final = edit.get("type") if isinstance(edit, dict) else None
|
||||
return isinstance(edit_type, str) and edit_type in BEDROCK_INVOKE_CONTEXT_MANAGEMENT_EDIT_BETAS
|
||||
|
||||
|
||||
def _invoke_context_management(context_management: CountContextManagement | None) -> Mapping[str, JsonValue]:
|
||||
spec: Final = context_management_with_edits(None if context_management is None else context_management.spec)
|
||||
edits: Final = spec.get("edits") if isinstance(spec, dict) else None
|
||||
supported_edits: Final = tuple(filter(_invoke_supports_edit, edits if isinstance(edits, list) else ()))
|
||||
if not isinstance(spec, dict) or not supported_edits:
|
||||
return MappingProxyType({})
|
||||
supported_spec: Final[JsonValue] = {**spec, "edits": list(supported_edits)}
|
||||
return MappingProxyType(
|
||||
{
|
||||
"context_management": supported_spec,
|
||||
"anthropic_beta": list(context_management_betas(supported_spec)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class BedrockTokenCounter(BaseTokenCounter):
|
||||
"""Counts on the configured Bedrock endpoint, with Mantle retry when Runtime rejects a Claude model"""
|
||||
|
||||
|
|
@ -167,11 +209,19 @@ class BedrockTokenCounter(BaseTokenCounter):
|
|||
)
|
||||
if isinstance(translated, ChatCountTranslationError):
|
||||
return translated.response(request_model, model_to_use, primary_tokenizer)
|
||||
runtime_request: Final = (
|
||||
_request_data(model_to_use, messages, tools, system)
|
||||
if translated is None
|
||||
else _request_data(model_to_use, translated.messages, translated.tools, translated.system)
|
||||
native_context_management: Final = _invoke_context_management(
|
||||
context_management if is_mantle_claude_model(resolved_model) else None
|
||||
)
|
||||
counted_messages: Final = messages if translated is None else translated.messages
|
||||
runtime_request: Final = {
|
||||
**_request_data(
|
||||
model_to_use,
|
||||
tuple(map(_invoke_model_message, counted_messages)) if native_context_management else counted_messages,
|
||||
tools if translated is None else translated.tools,
|
||||
system if translated is None else translated.system,
|
||||
),
|
||||
**native_context_management,
|
||||
}
|
||||
mantle_request: Final = runtime_request if translated is None else {**runtime_request, **translated.options}
|
||||
|
||||
primary: Final = (
|
||||
|
|
@ -184,8 +234,12 @@ class BedrockTokenCounter(BaseTokenCounter):
|
|||
)
|
||||
)
|
||||
outcome: Final = (
|
||||
await self._count_with(
|
||||
self._mantle_handler, MANTLE_TOKENIZER_TYPE, mantle_request, litellm_params, resolved_model
|
||||
_mantle_retry_outcome(
|
||||
primary,
|
||||
await self._count_with(
|
||||
self._mantle_handler, MANTLE_TOKENIZER_TYPE, mantle_request, litellm_params, resolved_model
|
||||
),
|
||||
carried_edits=bool(native_context_management),
|
||||
)
|
||||
if not use_mantle and _runtime_rejected_claude_model(primary, resolved_model)
|
||||
else primary
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from typing_extensions import NotRequired, ReadOnly, TypedDict
|
|||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.url_utils import join_api_base
|
||||
from litellm.llms.anthropic.common_utils import tools_without_custom_field
|
||||
from litellm.llms.anthropic.common_utils import context_management_betas, tools_without_custom_field
|
||||
from litellm.llms.anthropic.count_tokens.transformation import COUNT_TOKEN_OPTION_NAMES, AnthropicCountTokensConfig
|
||||
from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM, run_aws_signing
|
||||
from litellm.llms.bedrock.common_utils import BedrockError, build_mantle_messages_url
|
||||
|
|
@ -22,6 +22,7 @@ class MantleCountTokensRequest(TypedDict):
|
|||
messages: ReadOnly[list[dict[str, JsonValue]]]
|
||||
system: ReadOnly[NotRequired[JsonValue]]
|
||||
tools: ReadOnly[NotRequired[list[dict[str, JsonValue]]]]
|
||||
context_management: ReadOnly[NotRequired[JsonValue]]
|
||||
|
||||
|
||||
_COUNT_REQUEST: Final = TypeAdapter(MantleCountTokensRequest)
|
||||
|
|
@ -74,13 +75,19 @@ class BedrockMantleCountTokensHandler(BaseAWSLLM):
|
|||
optional_params=_COUNT_OPTIONS.validate_python(
|
||||
{name: value for name, value in request_data.items() if name in COUNT_TOKEN_OPTION_NAMES}
|
||||
),
|
||||
context_management=request.get("context_management"),
|
||||
)
|
||||
betas: Final = context_management_betas(request.get("context_management"))
|
||||
verbose_logger.debug("Making bedrock-mantle count_tokens request to: %s", endpoint_url)
|
||||
api_key: Final = litellm_params.get("api_key")
|
||||
signed_headers, signed_body = await run_aws_signing(
|
||||
self._sign_request,
|
||||
service_name="bedrock",
|
||||
headers={"Content-Type": "application/json", "anthropic-version": MANTLE_ANTHROPIC_VERSION},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"anthropic-version": MANTLE_ANTHROPIC_VERSION,
|
||||
**({"anthropic-beta": ",".join(betas)} if betas else {}),
|
||||
},
|
||||
optional_params=litellm_params,
|
||||
request_data=body,
|
||||
api_base=endpoint_url,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
|||
extract_file_data,
|
||||
text_completion_prompt_to_messages,
|
||||
)
|
||||
from litellm.llms.anthropic.common_utils import AnthropicError
|
||||
from litellm.llms.base_llm.base_utils import map_developer_role_to_system_role
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
from litellm.llms.base_llm.files.batch_records import responses_batch_body_to_chat_body
|
||||
|
|
@ -347,7 +348,14 @@ class _BatchInputLine:
|
|||
record: _OpenAIBatchRecord
|
||||
|
||||
|
||||
_BATCH_RECORD_CONVERSION_ERRORS: Final = (AttributeError, LookupError, TypeError, ValueError, BadRequestError)
|
||||
_BATCH_RECORD_CONVERSION_ERRORS: Final = (
|
||||
AttributeError,
|
||||
LookupError,
|
||||
TypeError,
|
||||
ValueError,
|
||||
BadRequestError,
|
||||
AnthropicError,
|
||||
)
|
||||
|
||||
|
||||
def _batch_line_error(line_number: int, problem: str) -> BedrockError:
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ from litellm.llms.bedrock.chat.invoke_transformations.base_invoke_transformation
|
|||
bedrock_invoke_guardrail_headers,
|
||||
)
|
||||
from litellm.llms.bedrock.common_utils import (
|
||||
BEDROCK_INVOKE_CONTEXT_MANAGEMENT_EDIT_BETAS,
|
||||
BedrockError,
|
||||
apply_bedrock_invoke_structured_output,
|
||||
bedrock_supports_tool_search,
|
||||
|
|
@ -499,12 +500,7 @@ class AmazonAnthropicClaudeMessagesConfig(
|
|||
# Bedrock InvokeModel DOES support ``clear_tool_uses_20250919`` under the
|
||||
# ``context-management-2025-06-27`` beta. AWS docs:
|
||||
# https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages-tool-use.md
|
||||
_BEDROCK_INVOKE_SUPPORTED_CONTEXT_MANAGEMENT_EDITS: Mapping[str, str] = MappingProxyType(
|
||||
{
|
||||
"compact_20260112": ANTHROPIC_BETA_HEADER_VALUES.COMPACT_2026_01_12.value,
|
||||
"clear_tool_uses_20250919": ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value,
|
||||
}
|
||||
)
|
||||
_BEDROCK_INVOKE_SUPPORTED_CONTEXT_MANAGEMENT_EDITS: Mapping[str, str] = BEDROCK_INVOKE_CONTEXT_MANAGEMENT_EDIT_BETAS
|
||||
|
||||
@classmethod
|
||||
def _filter_context_management_for_bedrock_invoke(
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ role / access key / profile / web identity), signed via the shared
|
|||
BaseAWSLLM._sign_request after the request body is finalized.
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from functools import partial
|
||||
from typing import Final, cast # noqa: TID251 # map_openai_params returns the filtered params as a bare dict
|
||||
|
||||
import httpx
|
||||
|
|
@ -23,6 +24,7 @@ import httpx
|
|||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.core_helpers import drops_unsupported_params
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import chat_completion_prompt_cache_breakpoint_for_wire
|
||||
from litellm.litellm_core_utils.url_utils import join_api_base
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
from litellm.llms.base_llm.responses.codex_compat import drop_unsupported_tools, normalize_codex_input_items
|
||||
|
|
@ -62,6 +64,10 @@ _BEDROCK_MANTLE_SUPPORTED_SERVICE_TIERS: Final = frozenset({"auto", "default"})
|
|||
_BEDROCK_MANTLE_OPENAI_PATH_SUPPORTED_REASONING_SUMMARIES: Final = frozenset({"auto"})
|
||||
|
||||
|
||||
def _prompt_cache_breakpoint_as_sent_or_none(marker: object) -> object:
|
||||
return marker if chat_completion_prompt_cache_breakpoint_for_wire(marker, drop_params=True) == marker else None
|
||||
|
||||
|
||||
class BedrockMantleResponsesAPIConfig(BedrockMantleAuthMixin, OpenAIResponsesAPIConfig):
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -113,6 +119,13 @@ class BedrockMantleResponsesAPIConfig(BedrockMantleAuthMixin, OpenAIResponsesAPI
|
|||
headers["OpenAI-Project"] = litellm_params.aws_bedrock_project_id
|
||||
return headers
|
||||
|
||||
def _prompt_cache_breakpoint_to_wire(
|
||||
self, litellm_params: GenericLiteLLMParams, reads_breakpoints: bool, known_support: bool | None
|
||||
) -> Callable[[object], object]:
|
||||
if reads_breakpoints or known_support is True:
|
||||
return partial(chat_completion_prompt_cache_breakpoint_for_wire, drop_params=True)
|
||||
return _prompt_cache_breakpoint_as_sent_or_none
|
||||
|
||||
def supports_native_file_search(self) -> bool:
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any, Final
|
|||
import httpx
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.core_helpers import map_finish_reason
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig
|
||||
from litellm.types.llms.cohere import CohereV2ChatResponse
|
||||
|
|
@ -248,6 +249,9 @@ class CohereV2ChatConfig(OpenAIGPTConfig):
|
|||
prompt_tokens: Final = token_usage.get("input_tokens", 0)
|
||||
completion_tokens: Final = token_usage.get("output_tokens", 0)
|
||||
|
||||
model_response.choices[0].finish_reason = map_finish_reason( # rebind-ok: ModelResponse is filled in place
|
||||
cohere_v2_chat_response.get("finish_reason", "COMPLETE")
|
||||
)
|
||||
model_response.created = int(time.time())
|
||||
model_response.model = model
|
||||
usage: Final = Usage(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from typing import Final, Literal
|
|||
|
||||
from litellm.llms.base_llm.base_utils import BaseLLMModelInfo
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
from litellm.types.llms.cohere import CohereV2StreamedToolCall
|
||||
from litellm.types.llms.openai import AllMessageValues
|
||||
from litellm.types.utils import (
|
||||
ChatCompletionToolCallChunk,
|
||||
|
|
@ -222,6 +223,7 @@ class CohereV2ModelResponseIterator:
|
|||
self.content_blocks: list = []
|
||||
self.tool_index = -1
|
||||
self.json_mode = json_mode
|
||||
self.tool_call_indexes_with_arguments: frozenset[int] = frozenset()
|
||||
|
||||
def _parse_content_delta(self, chunk: dict) -> str:
|
||||
"""Parse content-delta chunks to extract text."""
|
||||
|
|
@ -234,20 +236,23 @@ class CohereV2ModelResponseIterator:
|
|||
return content
|
||||
return ""
|
||||
|
||||
def _parse_tool_call_delta(self, chunk: dict) -> ChatCompletionToolCallChunk | None:
|
||||
"""Parse tool-call-delta chunks to extract tool calls."""
|
||||
delta: Final = chunk.get("delta", {})
|
||||
tool_calls: Final = delta.get("tool_calls", [])
|
||||
if tool_calls:
|
||||
return {
|
||||
"id": tool_calls[0].get("id", ""),
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_calls[0].get("name", ""),
|
||||
"arguments": tool_calls[0].get("arguments", ""),
|
||||
},
|
||||
}
|
||||
return None
|
||||
def _parse_tool_call_delta(self, chunk: dict, index: int) -> ChatCompletionToolCallChunk:
|
||||
"""Parse tool-call-start and tool-call-delta chunks to extract tool calls."""
|
||||
tool_call: Final = CohereV2StreamedToolCall.model_validate(chunk)
|
||||
if tool_call.arguments:
|
||||
self.tool_call_indexes_with_arguments = self.tool_call_indexes_with_arguments | {index}
|
||||
return {
|
||||
"id": tool_call.id,
|
||||
"type": "function",
|
||||
"function": {"name": tool_call.name, "arguments": tool_call.arguments},
|
||||
"index": index,
|
||||
}
|
||||
|
||||
def _parse_tool_call_end(self, index: int) -> ChatCompletionToolCallChunk | None:
|
||||
"""Close a tool call that streamed no arguments with an empty JSON object, as OpenAI streams one."""
|
||||
if index in self.tool_call_indexes_with_arguments:
|
||||
return None
|
||||
return {"id": None, "type": "function", "function": {"name": None, "arguments": "{}"}, "index": index}
|
||||
|
||||
def _parse_tool_plan_delta(self, chunk: dict) -> dict | None:
|
||||
"""Parse tool-plan-delta events to extract tool plan."""
|
||||
|
|
@ -301,7 +306,8 @@ class CohereV2ModelResponseIterator:
|
|||
|
||||
v2 format:
|
||||
- Content: chunk.type == "content-delta" -> chunk.delta.message.content.text
|
||||
- Tool calls: chunk.type == "tool-call-delta" -> chunk.delta.tool_calls
|
||||
- Tool calls: chunk.type in ("tool-call-start", "tool-call-delta") -> chunk.delta.message.tool_calls
|
||||
- Tool call end: chunk.type == "tool-call-end" -> "{}" arguments when the call streamed none
|
||||
- Tool plan: chunk.event == "tool-plan-delta" -> chunk.data.delta.message.tool_plan
|
||||
- Citations: chunk.event == "citation-start" -> chunk.data.delta.message.citations
|
||||
- Finish: chunk.event == "message-end" -> chunk.data.delta.finish_reason
|
||||
|
|
@ -321,8 +327,12 @@ class CohereV2ModelResponseIterator:
|
|||
# Handle different chunk types
|
||||
if chunk_type == "content-delta":
|
||||
text = self._parse_content_delta(chunk)
|
||||
elif chunk_type == "tool-call-delta":
|
||||
tool_use = self._parse_tool_call_delta(chunk)
|
||||
elif chunk_type in ("tool-call-start", "tool-call-delta", "tool-call-end"):
|
||||
tool_use = (
|
||||
self._parse_tool_call_end(index)
|
||||
if chunk_type == "tool-call-end"
|
||||
else self._parse_tool_call_delta(chunk, index)
|
||||
)
|
||||
elif event_type == "tool-plan-delta":
|
||||
provider_specific_fields = self._parse_tool_plan_delta(chunk)
|
||||
elif event_type == "citation-start":
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from litellm.llms.base_llm.chat.transformation import BaseConfig
|
|||
from litellm.llms.base_llm.image_variations.transformation import (
|
||||
BaseImageVariationConfig,
|
||||
)
|
||||
from litellm.llms.custom_httpx.aiohttp_pool_diagnostics import build_pool_wait_trace_config
|
||||
from litellm.llms.custom_httpx.aiohttp_transport import LiteLLMAiohttpTransport
|
||||
from litellm.llms.custom_httpx.http_handler import (
|
||||
AsyncHTTPHandler,
|
||||
|
|
@ -90,7 +91,7 @@ class BaseLLMAIOHTTPHandler:
|
|||
if self.transport is None:
|
||||
connector: Final = self._get_connector()
|
||||
if connector:
|
||||
return aiohttp.ClientSession(connector=connector)
|
||||
return aiohttp.ClientSession(connector=connector, trace_configs=[build_pool_wait_trace_config()])
|
||||
|
||||
transport: Final = self.transport or self._get_or_create_transport()
|
||||
if transport is not None and hasattr(transport, "_get_valid_client_session"):
|
||||
|
|
@ -99,7 +100,7 @@ class BaseLLMAIOHTTPHandler:
|
|||
except RuntimeError:
|
||||
pass
|
||||
|
||||
return aiohttp.ClientSession()
|
||||
return aiohttp.ClientSession(trace_configs=[build_pool_wait_trace_config()])
|
||||
|
||||
def _get_async_client_session(self, dynamic_client_session: ClientSession | None = None) -> ClientSession:
|
||||
if dynamic_client_session:
|
||||
|
|
|
|||
130
litellm/llms/custom_httpx/aiohttp_pool_diagnostics.py
Normal file
130
litellm/llms/custom_httpx/aiohttp_pool_diagnostics.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from typing import Final
|
||||
|
||||
from aiohttp import (
|
||||
ClientSession,
|
||||
TraceConfig,
|
||||
TraceConnectionQueuedEndParams,
|
||||
TraceConnectionQueuedStartParams,
|
||||
TraceRequestExceptionParams,
|
||||
TraceRequestRedirectParams,
|
||||
TraceRequestStartParams,
|
||||
)
|
||||
from yarl import URL
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
|
||||
_POOL_WAIT_WARNING_SECONDS: Final = 1.0
|
||||
|
||||
|
||||
class _PoolWaitContext(SimpleNamespace):
|
||||
def __init__(self, trace_request_ctx: object = None) -> None:
|
||||
super().__init__()
|
||||
self.host: str | None = None
|
||||
self.port: int | None = None
|
||||
self.started_at: float | None = None
|
||||
self.waited: float = 0.0
|
||||
self.warning: asyncio.TimerHandle | None = None
|
||||
self.warned: bool = False
|
||||
|
||||
def set_destination(self, params: TraceRequestStartParams) -> None:
|
||||
self.host = params.url.host
|
||||
self.port = params.url.port
|
||||
|
||||
def redirect(self, params: TraceRequestRedirectParams) -> None:
|
||||
location: Final = params.response.headers.get("Location") or params.response.headers.get("URI")
|
||||
if location is None:
|
||||
return
|
||||
try:
|
||||
destination: Final = params.url.join(URL(location))
|
||||
host: Final = destination.host
|
||||
port: Final = destination.port
|
||||
except ValueError:
|
||||
return
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
def start(self, session: ClientSession) -> None:
|
||||
loop: Final = asyncio.get_running_loop()
|
||||
self.started_at = loop.time()
|
||||
if not self.warned:
|
||||
self.warning = loop.call_later(max(0.0, _POOL_WAIT_WARNING_SECONDS - self.waited), self.warn, session)
|
||||
|
||||
def elapsed(self) -> float:
|
||||
return self.waited + (
|
||||
asyncio.get_running_loop().time() - self.started_at if self.started_at is not None else 0.0
|
||||
)
|
||||
|
||||
def warn(self, session: ClientSession) -> None:
|
||||
if self.warned or self.started_at is None:
|
||||
return
|
||||
self.warned = True
|
||||
connector: Final = session.connector
|
||||
if connector is None:
|
||||
return
|
||||
verbose_logger.warning(
|
||||
"aiohttp connection pool saturated: host=%s port=%s waited=%.3fs for a connection slot "
|
||||
"(acquired_total=%s, limit=%s, limit_per_host=%s). "
|
||||
"Wait occurred before sending the request upstream.",
|
||||
self.host,
|
||||
self.port,
|
||||
self.elapsed(),
|
||||
len(connector._acquired), # pyright: ignore[reportPrivateUsage] # aiohttp exposes no public acquired count
|
||||
connector.limit,
|
||||
connector.limit_per_host,
|
||||
)
|
||||
|
||||
def finish(self, session: ClientSession, failed: bool = False) -> None:
|
||||
if self.warning is not None:
|
||||
self.warning.cancel()
|
||||
self.warning = None
|
||||
if self.started_at is None:
|
||||
return
|
||||
if failed or self.elapsed() >= _POOL_WAIT_WARNING_SECONDS:
|
||||
self.warn(session)
|
||||
self.waited = self.elapsed()
|
||||
self.started_at = None
|
||||
|
||||
|
||||
async def _on_request_start(session: ClientSession, context: SimpleNamespace, params: TraceRequestStartParams) -> None:
|
||||
if isinstance(context, _PoolWaitContext):
|
||||
context.set_destination(params)
|
||||
|
||||
|
||||
async def _on_queued_start(
|
||||
session: ClientSession, context: SimpleNamespace, params: TraceConnectionQueuedStartParams
|
||||
) -> None:
|
||||
if isinstance(context, _PoolWaitContext):
|
||||
context.start(session)
|
||||
|
||||
|
||||
async def _on_queued_end(
|
||||
session: ClientSession, context: SimpleNamespace, params: TraceConnectionQueuedEndParams
|
||||
) -> None:
|
||||
if isinstance(context, _PoolWaitContext):
|
||||
context.finish(session)
|
||||
|
||||
|
||||
async def _on_request_exception(
|
||||
session: ClientSession, context: SimpleNamespace, params: TraceRequestExceptionParams
|
||||
) -> None:
|
||||
if isinstance(context, _PoolWaitContext):
|
||||
context.finish(session, failed=True)
|
||||
|
||||
|
||||
async def _on_request_redirect(
|
||||
session: ClientSession, context: SimpleNamespace, params: TraceRequestRedirectParams
|
||||
) -> None:
|
||||
if isinstance(context, _PoolWaitContext):
|
||||
context.redirect(params)
|
||||
|
||||
|
||||
def build_pool_wait_trace_config() -> TraceConfig:
|
||||
trace: Final = TraceConfig(trace_config_ctx_factory=_PoolWaitContext)
|
||||
trace.on_request_start.append(_on_request_start)
|
||||
trace.on_connection_queued_start.append(_on_queued_start)
|
||||
trace.on_connection_queued_end.append(_on_queued_end)
|
||||
trace.on_request_exception.append(_on_request_exception)
|
||||
trace.on_request_redirect.append(_on_request_redirect)
|
||||
return trace
|
||||
|
|
@ -44,6 +44,7 @@ from litellm.litellm_core_utils.logging_utils import track_llm_api_timing
|
|||
from litellm.litellm_core_utils.request_timeout_resolver import (
|
||||
get_configured_request_timeout,
|
||||
)
|
||||
from litellm.llms.custom_httpx.aiohttp_pool_diagnostics import build_pool_wait_trace_config
|
||||
from litellm.llms.custom_httpx.http2_connection_spread import AsyncHTTP2ConnectionSpread
|
||||
from litellm.types.llms.custom_http import *
|
||||
|
||||
|
|
@ -1456,6 +1457,7 @@ class AsyncHTTPHandler:
|
|||
connector=TCPConnector(**transport_connector_kwargs),
|
||||
cookie_jar=DummyCookieJar(),
|
||||
trust_env=trust_env,
|
||||
trace_configs=[build_pool_wait_trace_config()],
|
||||
)
|
||||
|
||||
# Use shared session if provided and valid
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ Translates from OpenAI's `/v1/chat/completions` to Databricks' `/chat/completion
|
|||
|
||||
import os
|
||||
from collections.abc import AsyncIterator, Coroutine, Iterator, Mapping, Sequence
|
||||
from itertools import chain, takewhile
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, cast, overload
|
||||
|
||||
|
|
@ -12,6 +13,7 @@ from pydantic import BaseModel, ConfigDict, TypeAdapter
|
|||
|
||||
from litellm.anthropic_beta_gated_fields import log_beta_gated_strips, strip_beta_gated_thinking
|
||||
from litellm.constants import RESPONSE_FORMAT_TOOL_NAME
|
||||
from litellm.litellm_core_utils.core_helpers import drops_unsupported_params
|
||||
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
|
||||
handle_invalid_parallel_tool_calls,
|
||||
should_convert_tool_call_to_json_mode,
|
||||
|
|
@ -23,6 +25,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
|||
strip_name_from_message,
|
||||
strip_prefill_prefix_field,
|
||||
)
|
||||
from litellm.litellm_core_utils.tool_fields import tool_params_without_hosted_mcp
|
||||
from litellm.litellm_core_utils.url_utils import join_api_base
|
||||
from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator
|
||||
from litellm.llms.base_llm.base_utils import (
|
||||
|
|
@ -65,7 +68,7 @@ from ...anthropic.chat.transformation import (
|
|||
tools_without_unsupported_tool_search,
|
||||
)
|
||||
from ...openai_like.chat.transformation import OpenAILikeChatConfig
|
||||
from ..common_utils import DatabricksBase, DatabricksException
|
||||
from ..common_utils import DATABRICKS_HOSTED_MODEL_PREFIXES, DatabricksBase, DatabricksException
|
||||
|
||||
_RESPONSE_FORMAT_ADAPTER: ( # mutable-ok: mirrors the dict return contract of get_json_schema_from_pydantic_object
|
||||
Final[TypeAdapter[dict[str, object] | None]]
|
||||
|
|
@ -81,15 +84,139 @@ def _is_bare_assistant_message(message_dict: Mapping[str, object]) -> bool:
|
|||
)
|
||||
|
||||
|
||||
_CONTENT_BLOCKS: Final = TypeAdapter(tuple[object, ...])
|
||||
|
||||
|
||||
def _content_blocks(message: object) -> tuple[object, ...]:
|
||||
match message.model_dump(exclude_none=True) if isinstance(message, BaseModel) else message:
|
||||
case {"content": list() as content}:
|
||||
return _CONTENT_BLOCKS.validate_python(content)
|
||||
case _:
|
||||
return ()
|
||||
|
||||
|
||||
def _plain_text(block: object) -> str | None:
|
||||
match block:
|
||||
case {"type": "text", "text": str() as text} if block == {"type": "text", "text": text}:
|
||||
return text
|
||||
case _:
|
||||
return None
|
||||
|
||||
|
||||
def _plain_texts(message: Mapping[str, object]) -> tuple[str, ...]:
|
||||
blocks: Final = _content_blocks(message)
|
||||
texts: Final = tuple(text for block in blocks if (text := _plain_text(block)) is not None)
|
||||
return texts if len(texts) == len(blocks) else ()
|
||||
|
||||
|
||||
def _collapse_single_text_block(message: Mapping[str, object]) -> Mapping[str, object]:
|
||||
content: Final = message.get("content")
|
||||
match content:
|
||||
case [{"type": "text", "text": str() as text}] if content == [{"type": "text", "text": text}]:
|
||||
match _plain_texts(message):
|
||||
case (text,):
|
||||
return {**message, "content": text}
|
||||
case _:
|
||||
return message
|
||||
|
||||
|
||||
def _without_text_cache_control(block: object) -> object:
|
||||
match block:
|
||||
case {"type": "text", "text": str() as text, "cache_control": object() as cache_control} if block == {
|
||||
"type": "text",
|
||||
"text": text,
|
||||
"cache_control": cache_control,
|
||||
}:
|
||||
return {"type": "text", "text": text}
|
||||
case _:
|
||||
return block
|
||||
|
||||
|
||||
def _without_cache_control(message: AllMessageValues) -> AllMessageValues:
|
||||
uncached: Final = {key: value for key, value in message.items() if key != "cache_control"}
|
||||
match content := uncached.get("content"):
|
||||
case list():
|
||||
blocks: Final = [_without_text_cache_control(block) for block in _CONTENT_BLOCKS.validate_python(content)]
|
||||
return cast(AllMessageValues, {**uncached, "content": blocks}) # cast-ok: spread keeps the shape
|
||||
case _:
|
||||
return cast(AllMessageValues, uncached) # cast-ok: only cache_control was left out
|
||||
|
||||
|
||||
def _has_string_content(message: Mapping[str, object]) -> bool:
|
||||
return isinstance(message.get("content"), str)
|
||||
|
||||
|
||||
def _is_system(message: Mapping[str, object]) -> bool:
|
||||
return message.get("role") == "system"
|
||||
|
||||
|
||||
def _json_mode_message(message: AllMessageValues, drop_params: bool) -> AllMessageValues:
|
||||
candidate: Final = _without_cache_control(message) if drop_params else message
|
||||
texts: Final = _plain_texts(candidate)
|
||||
if "cache_control" in candidate or not (texts or _has_string_content(candidate)):
|
||||
return message
|
||||
if sum(1 for text in texts if text.strip()) < 2:
|
||||
return candidate
|
||||
return cast(AllMessageValues, {**candidate, "content": "".join(texts)}) # cast-ok: spread keeps the shape
|
||||
|
||||
|
||||
def _is_dropped_on_send(message: Mapping[str, object]) -> bool:
|
||||
sanitized: Final = {**message}
|
||||
_sanitize_empty_content(sanitized)
|
||||
return _is_bare_assistant_message(sanitized)
|
||||
|
||||
|
||||
def _is_system_or_dropped(message: Mapping[str, object]) -> bool:
|
||||
return _is_system(message) or _is_dropped_on_send(message)
|
||||
|
||||
|
||||
def _json_checked_start(messages: Sequence[AllMessageValues]) -> int:
|
||||
trailing: Final = tuple(takewhile(_is_system_or_dropped, reversed(messages)))
|
||||
last_sent_offset: Final = 0 if any(_is_system(message) for message in trailing) else 1
|
||||
return max(len(messages) - len(trailing) - last_sent_offset, 0)
|
||||
|
||||
|
||||
def _json_object_ready_messages(
|
||||
model: str,
|
||||
messages: list[AllMessageValues], # mutable-ok: inherited transform_request contract
|
||||
response_format: object,
|
||||
drop_params: bool,
|
||||
) -> list[AllMessageValues]: # mutable-ok: inherited transform_request contract
|
||||
match response_format:
|
||||
case {"type": "json_object"} if "claude" not in model and model.startswith(DATABRICKS_HOSTED_MODEL_PREFIXES):
|
||||
checked_start: Final = _json_checked_start(messages)
|
||||
return [
|
||||
*messages[:checked_start],
|
||||
*(_json_mode_message(message, drop_params) for message in messages[checked_start:]),
|
||||
]
|
||||
case _:
|
||||
return messages
|
||||
|
||||
|
||||
def _is_empty_text_block(block: object) -> bool:
|
||||
match block:
|
||||
case {"type": "text", "text": str() as text}:
|
||||
return not text.strip()
|
||||
case {"type": "text", "text": object() as text}:
|
||||
return not text
|
||||
case {"type": "text"}:
|
||||
return True
|
||||
case _:
|
||||
return False
|
||||
|
||||
|
||||
def _non_string_text_type(block: object) -> str | None:
|
||||
match block:
|
||||
case {"type": "text", "text": str()}:
|
||||
return None
|
||||
case {"type": "text", "text": object() as text} if text:
|
||||
return type(text).__name__
|
||||
case _:
|
||||
return None
|
||||
|
||||
|
||||
def _first_non_string_text_type(messages: Sequence[AllMessageValues]) -> str | None:
|
||||
blocks: Final = chain.from_iterable(map(_content_blocks, messages))
|
||||
return next((type_name for block in blocks if (type_name := _non_string_text_type(block))), None)
|
||||
|
||||
|
||||
def _sanitize_empty_content(message_dict: dict[str, object]) -> None:
|
||||
"""
|
||||
Remove or filter content so empty text blocks are not sent.
|
||||
|
|
@ -108,9 +235,7 @@ def _sanitize_empty_content(message_dict: dict[str, object]) -> None:
|
|||
message_dict.pop("content")
|
||||
return
|
||||
filtered: Final = [
|
||||
block
|
||||
for block in content
|
||||
if not (isinstance(block, dict) and block.get("type") == "text" and not (block.get("text") or "").strip())
|
||||
block for block in _CONTENT_BLOCKS.validate_python(content) if not _is_empty_text_block(block)
|
||||
]
|
||||
if not filtered:
|
||||
message_dict.pop("content")
|
||||
|
|
@ -343,7 +468,7 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
|
|||
use_ai_gateway: Final = self.uses_ai_gateway(model)
|
||||
return join_api_base(self._get_api_base(api_base, use_ai_gateway=use_ai_gateway), "chat/completions")
|
||||
|
||||
def get_supported_openai_params(self, model: str | None = None) -> list:
|
||||
def get_supported_openai_params(self, model: str | None = None) -> list[str]:
|
||||
return [
|
||||
"stream",
|
||||
"stop",
|
||||
|
|
@ -449,8 +574,13 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
|
|||
drop_params: bool,
|
||||
replace_max_completion_tokens_with_max_tokens: bool = True,
|
||||
) -> dict:
|
||||
is_thinking_enabled: Final = self.is_thinking_enabled(non_default_params)
|
||||
mapped_params: Final = super().map_openai_params(non_default_params, optional_params, model, drop_params)
|
||||
request_params: Final = (
|
||||
tool_params_without_hosted_mcp(non_default_params, model=model, route="databricks", drop_params=drop_params)
|
||||
if "claude" in model
|
||||
else non_default_params
|
||||
)
|
||||
is_thinking_enabled: Final = self.is_thinking_enabled(request_params)
|
||||
mapped_params: Final = super().map_openai_params(request_params, optional_params, model, drop_params)
|
||||
if "claude" in model:
|
||||
AnthropicConfig.translate_legacy_thinking_for_adaptive_model(
|
||||
model=model, optional_params=mapped_params, custom_llm_provider="databricks", drop_params=drop_params
|
||||
|
|
@ -459,16 +589,16 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
|
|||
mapped_params["tools"] = self._map_openai_to_dbrx_tool(
|
||||
model=model, tools=mapped_params["tools"], drop_params=drop_params
|
||||
)
|
||||
if "max_completion_tokens" in non_default_params and replace_max_completion_tokens_with_max_tokens:
|
||||
mapped_params["max_tokens"] = non_default_params[
|
||||
if "max_completion_tokens" in request_params and replace_max_completion_tokens_with_max_tokens:
|
||||
mapped_params["max_tokens"] = request_params[
|
||||
"max_completion_tokens"
|
||||
] # most openai-compatible providers support 'max_tokens' not 'max_completion_tokens'
|
||||
mapped_params.pop("max_completion_tokens", None)
|
||||
|
||||
if "response_format" in non_default_params and "claude" in model:
|
||||
if "response_format" in request_params and "claude" in model:
|
||||
_tool: Final = self.map_response_format_to_databricks_tool(
|
||||
model,
|
||||
non_default_params["response_format"],
|
||||
request_params["response_format"],
|
||||
mapped_params,
|
||||
is_thinking_enabled,
|
||||
)
|
||||
|
|
@ -486,8 +616,8 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
|
|||
"response_format", None
|
||||
) # unsupported for claude models - if json_schema -> convert to tool call
|
||||
|
||||
if "reasoning_effort" in non_default_params and self._uses_anthropic_thinking_param(model):
|
||||
reasoning_effort_value: Final = non_default_params.get("reasoning_effort")
|
||||
if "reasoning_effort" in request_params and self._uses_anthropic_thinking_param(model):
|
||||
reasoning_effort_value: Final = request_params.get("reasoning_effort")
|
||||
mapped_thinking: Final = AnthropicConfig._map_reasoning_effort(
|
||||
reasoning_effort=reasoning_effort_value,
|
||||
model=model,
|
||||
|
|
@ -513,7 +643,7 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
|
|||
optional_params.pop("reasoning_effort", None)
|
||||
## handle thinking tokens
|
||||
self.update_optional_params_with_thinking_tokens(
|
||||
non_default_params=non_default_params, optional_params=mapped_params
|
||||
non_default_params=request_params, optional_params=mapped_params
|
||||
)
|
||||
gated_thinking: Final = strip_beta_gated_thinking(
|
||||
mapped_params.get("thinking"), # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] # mapped_params is the base config's bare dict
|
||||
|
|
@ -535,6 +665,35 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
|
|||
|
||||
return False
|
||||
|
||||
def transform_request(
|
||||
self,
|
||||
model: str,
|
||||
messages: list[AllMessageValues], # mutable-ok: inherited contract
|
||||
optional_params: dict[str, object], # mutable-ok: inherited contract
|
||||
litellm_params: dict[str, object], # mutable-ok: inherited contract
|
||||
headers: dict[str, object], # mutable-ok: inherited contract
|
||||
) -> dict[str, object]: # mutable-ok: inherited contract
|
||||
match _first_non_string_text_type(messages):
|
||||
case str() as type_name:
|
||||
raise DatabricksException(
|
||||
status_code=400,
|
||||
message=f"A text content block needs a string 'text', got {type_name}",
|
||||
)
|
||||
case None:
|
||||
pass
|
||||
return super().transform_request(
|
||||
model=model,
|
||||
messages=_json_object_ready_messages(
|
||||
model,
|
||||
messages,
|
||||
optional_params.get("response_format"),
|
||||
drop_params=drops_unsupported_params(litellm_params.get("drop_params")),
|
||||
),
|
||||
optional_params=optional_params,
|
||||
litellm_params=litellm_params,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
@overload
|
||||
def _transform_messages(
|
||||
self, messages: list[AllMessageValues], model: str, is_async: Literal[True]
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ DATABRICKS_WORKSPACE_ROUTE_PREFIXES: Final = (
|
|||
DATABRICKS_AI_GATEWAY_MESSAGES_PATH,
|
||||
)
|
||||
DATABRICKS_NATIVE_MESSAGES_MODEL_PREFIXES: Final = ("databricks-claude-", "system.ai.claude-")
|
||||
DATABRICKS_HOSTED_MODEL_PREFIXES: Final = ("databricks-", "system.ai.")
|
||||
DATABRICKS_ENDPOINT_PATHS: Final = MappingProxyType(
|
||||
{
|
||||
"chat_completions": "/chat/completions",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
|||
extract_search_results_text,
|
||||
)
|
||||
from litellm.litellm_core_utils.reasoning_effort_utils import OPAQUE_VALUE, warn_dropped_reasoning
|
||||
from litellm.litellm_core_utils.tool_fields import tool_params_without_hosted_mcp
|
||||
from litellm.litellm_core_utils.url_utils import ensure_api_route
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.llms.openai import AllMessageValues
|
||||
|
|
@ -33,17 +34,15 @@ def _warn_on_dropped_budget(thinking: object, model: str) -> None:
|
|||
|
||||
|
||||
class DeepSeekChatConfig(OpenAIGPTConfig):
|
||||
def get_supported_openai_params(self, model: str) -> list:
|
||||
def get_supported_openai_params(self, model: str) -> list[str]:
|
||||
"""
|
||||
DeepSeek reasoner models support thinking parameter.
|
||||
"""
|
||||
params: Final = super().get_supported_openai_params(model)
|
||||
params.extend(["thinking", "reasoning_effort"])
|
||||
return params
|
||||
return [*super().get_supported_openai_params(model), "thinking", "reasoning_effort"]
|
||||
|
||||
def map_openai_params(
|
||||
self,
|
||||
non_default_params: dict,
|
||||
non_default_params: dict[str, object],
|
||||
optional_params: dict,
|
||||
model: str,
|
||||
drop_params: bool,
|
||||
|
|
@ -59,7 +58,12 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
|
|||
Reference: https://api-docs.deepseek.com/guides/thinking_mode
|
||||
"""
|
||||
# Let parent handle standard params first
|
||||
optional_params = super().map_openai_params(non_default_params, optional_params, model, drop_params)
|
||||
optional_params = super().map_openai_params(
|
||||
tool_params_without_hosted_mcp(non_default_params, model=model, route="deepseek", drop_params=drop_params),
|
||||
optional_params,
|
||||
model,
|
||||
drop_params,
|
||||
)
|
||||
|
||||
# Pop thinking/reasoning_effort from optional_params first (parent may have added them)
|
||||
# Then re-add only if valid for DeepSeek
|
||||
|
|
@ -265,6 +269,7 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
|
|||
tools: Final = optional_params.get("tools")
|
||||
if not isinstance(tools, list) or not tools:
|
||||
return optional_params
|
||||
sent_tools: Final[tuple[object, ...]] = tuple(tools)
|
||||
|
||||
def _is_function_tool(tool: object) -> bool:
|
||||
return isinstance(tool, dict) and tool.get("type") == "function"
|
||||
|
|
@ -289,14 +294,14 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
|
|||
name: Final = function.get("name")
|
||||
return isinstance(name, str) and name in function_tool_names
|
||||
|
||||
function_tools: Final = [tool for tool in tools if _is_function_tool(tool)]
|
||||
if len(function_tools) == len(tools):
|
||||
function_tools: Final = [tool for tool in sent_tools if _is_function_tool(tool)]
|
||||
if len(function_tools) == len(sent_tools):
|
||||
return optional_params
|
||||
|
||||
dropped_types: Final = sorted(
|
||||
{
|
||||
str(tool.get("type")) if isinstance(tool, dict) else type(tool).__name__
|
||||
for tool in tools
|
||||
for tool in sent_tools
|
||||
if not _is_function_tool(tool)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ from litellm.utils import (
|
|||
declared_mid_conversation_system_support,
|
||||
deployment_declared_mid_conversation_system_support,
|
||||
deployment_model_info,
|
||||
get_bundled_model_cost_map,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -54,6 +55,15 @@ def _forwarded_user_params(litellm_params: GenericLiteLLMParams) -> Mapping[str,
|
|||
)
|
||||
|
||||
|
||||
def _bundled_cost_map_declares_mid_conversation_system(model: str) -> bool:
|
||||
bundled_cost_map: Final[Mapping[str, object]] = get_bundled_model_cost_map()
|
||||
names: Final = (model.removeprefix("fireworks_ai/"), resolve_fireworks_resource_name(model))
|
||||
return any(
|
||||
deployment_declared_mid_conversation_system_support(bundled_cost_map.get(f"fireworks_ai/{name}")) is True
|
||||
for name in names
|
||||
)
|
||||
|
||||
|
||||
class FireworksAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
||||
@property
|
||||
def custom_llm_provider(self) -> LlmProviders:
|
||||
|
|
@ -90,9 +100,13 @@ class FireworksAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
|||
validated_input: Final = self._validate_input_param(input)
|
||||
model_info: Final = deployment_model_info(litellm_params)
|
||||
declared: Final = declared_mid_conversation_system_support(model, self.custom_llm_provider.value, model_info)
|
||||
keeps_developer_items_in_place: Final = declared is True and (
|
||||
deployment_declared_mid_conversation_system_support(model_info) is True
|
||||
or _bundled_cost_map_declares_mid_conversation_system(model)
|
||||
)
|
||||
folded: Final = (
|
||||
None
|
||||
if deployment_declared_mid_conversation_system_support(model_info) is True
|
||||
if keeps_developer_items_in_place
|
||||
else instruction_items_folded(
|
||||
validated_input, response_api_optional_request_params, "user" if declared is False else "system"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from collections.abc import Mapping
|
||||
from typing import Final, cast
|
||||
from typing import ClassVar, Final, cast
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
|
|
@ -49,6 +49,8 @@ class GoogleAIStudioGeminiConfig(VertexGeminiConfig):
|
|||
Note: Please make sure to modify the default parameters as required for your use case.
|
||||
"""
|
||||
|
||||
_route: ClassVar[str] = "gemini"
|
||||
|
||||
temperature: float | None = None
|
||||
max_output_tokens: int | None = None
|
||||
top_p: float | None = None
|
||||
|
|
|
|||
|
|
@ -537,13 +537,16 @@ class GoogleAIStudioTokenCounter(BaseTokenCounter):
|
|||
) -> TokenCountResponse | None:
|
||||
import copy
|
||||
|
||||
from litellm.llms.anthropic.messages.context_management.inference_view import (
|
||||
InferenceRequestViewError,
|
||||
count_context_edits,
|
||||
inference_request_view,
|
||||
)
|
||||
from litellm.llms.gemini.count_tokens.handler import GoogleAIStudioTokenCounter
|
||||
from litellm.llms.vertex_ai.count_tokens.context import (
|
||||
VertexGeminiCountContextError,
|
||||
count_context_edits,
|
||||
count_context_error_response,
|
||||
gemini_count_contents,
|
||||
inference_request_view,
|
||||
malformed_tool_result_text_count_error,
|
||||
provider_error_response,
|
||||
unconvertible_messages_error,
|
||||
|
|
@ -556,7 +559,7 @@ class GoogleAIStudioTokenCounter(BaseTokenCounter):
|
|||
messages_format,
|
||||
count_context_edits(context_management, tools, (deployment or {}).get("litellm_params", {}), model_to_use),
|
||||
)
|
||||
if isinstance(request_view, VertexGeminiCountContextError):
|
||||
if isinstance(request_view, InferenceRequestViewError):
|
||||
return count_context_error_response(request_view, request_model, model_to_use)
|
||||
count_context: Final = vertex_gemini_count_context(system=request_view.system, tools=tools, model=model_to_use)
|
||||
if isinstance(count_context, VertexGeminiCountContextError):
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import httpx
|
|||
from pydantic import TypeAdapter
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.tool_fields import tool_params_without_hosted_mcp
|
||||
from litellm.litellm_core_utils.url_utils import join_api_base
|
||||
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
|
||||
from litellm.llms.gigachat.utils import convert_usage, get_api_base
|
||||
|
|
@ -169,7 +170,10 @@ class GigaChatConfig(BaseConfig):
|
|||
drop_params: bool,
|
||||
) -> dict: # mutable-ok: base class contract returns dict
|
||||
"""Map OpenAI parameters to GigaChat parameters."""
|
||||
for param, value in non_default_params.items():
|
||||
request_params: Final = tool_params_without_hosted_mcp(
|
||||
dict(non_default_params), model=model, route="gigachat", drop_params=drop_params
|
||||
)
|
||||
for param, value in request_params.items():
|
||||
if param == "stream":
|
||||
optional_params["stream"] = value
|
||||
elif param == "temperature":
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ async def search_skills(
|
|||
user_api_key_dict: UserAPIKeyAuth,
|
||||
proxy_logging_obj: ProxyLogging,
|
||||
) -> SkillSearchOutcome:
|
||||
if embedding_model is None:
|
||||
if embedding_model is None or not embedding_model.strip():
|
||||
return SkillSearchNotConfigured(
|
||||
reason="skill search needs litellm_settings.skill_search_embedding_model set to an embedding model from model_list"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from collections.abc import AsyncIterator, Iterator, Mapping
|
|||
from typing import TYPE_CHECKING, Any, Final, cast
|
||||
|
||||
from httpx._models import Headers, Response
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, TypeAdapter
|
||||
|
||||
import litellm
|
||||
from litellm._uuid import uuid
|
||||
|
|
@ -15,6 +15,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
|||
extract_reasoning_content,
|
||||
)
|
||||
from litellm.litellm_core_utils.reasoning_effort_utils import OPAQUE_VALUE
|
||||
from litellm.litellm_core_utils.tool_fields import tool_params_without_hosted_mcp
|
||||
from litellm.litellm_core_utils.url_utils import ensure_api_route
|
||||
from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator
|
||||
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
|
||||
|
|
@ -40,6 +41,8 @@ if TYPE_CHECKING:
|
|||
else:
|
||||
LiteLLMLoggingObj = Any
|
||||
|
||||
_REQUEST_PARAMS: Final = TypeAdapter(dict[str, object])
|
||||
|
||||
|
||||
class OllamaChatConfig(BaseConfig):
|
||||
"""
|
||||
|
|
@ -152,7 +155,13 @@ class OllamaChatConfig(BaseConfig):
|
|||
model: str,
|
||||
drop_params: bool,
|
||||
) -> dict:
|
||||
for param, value in non_default_params.items():
|
||||
request_params: Final = tool_params_without_hosted_mcp(
|
||||
_REQUEST_PARAMS.validate_python(non_default_params),
|
||||
model=model,
|
||||
route="ollama_chat",
|
||||
drop_params=drop_params,
|
||||
)
|
||||
for param, value in request_params.items():
|
||||
if param == "max_tokens" or param == "max_completion_tokens":
|
||||
optional_params["num_predict"] = value
|
||||
if param == "stream":
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response impo
|
|||
should_convert_tool_call_to_json_mode,
|
||||
)
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
chat_completion_prompt_cache_breakpoints_for_wire,
|
||||
drop_non_python_regex_patterns,
|
||||
drop_tool_reference_parts_from_tool_messages,
|
||||
flatten_combinators_and_drop_non_python_regex_patterns,
|
||||
|
|
@ -556,9 +557,10 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig):
|
|||
"""
|
||||
A deployment that reads OpenAI's ``prompt_cache_breakpoint`` (gpt-5.6+ on
|
||||
api.openai.com, or one opted in through ``prompt_cache_options`` or the cost
|
||||
map) gets each Anthropic ``cache_control`` mark rewritten in place. Any other
|
||||
endpoint keeps or strips the marks as before, and under drop_params also loses
|
||||
every ``prompt_cache_breakpoint`` it cannot read.
|
||||
map) gets each Anthropic ``cache_control`` mark rewritten in place, and under
|
||||
drop_params each content part's ``prompt_cache_breakpoint`` cut down to what chat
|
||||
completions accepts. Any other endpoint keeps or strips the marks as before, and
|
||||
under drop_params also loses every ``prompt_cache_breakpoint`` it cannot read.
|
||||
"""
|
||||
from litellm.integrations.anthropic_cache_control_hook import AnthropicCacheControlHook
|
||||
|
||||
|
|
@ -572,9 +574,15 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig):
|
|||
_claude_messages_within_cache_control_cap(model, messages, tools, litellm_params, request_params),
|
||||
tools,
|
||||
)
|
||||
drops_breakpoints: Final = not reads_breakpoints and _drops_params(litellm_params)
|
||||
drop_params: Final = _drops_params(litellm_params)
|
||||
drops_breakpoints: Final = not reads_breakpoints and drop_params
|
||||
renamed: Final = (
|
||||
[AnthropicCacheControlHook.cache_control_as_prompt_cache_breakpoints(message) for message in messages]
|
||||
[
|
||||
AnthropicCacheControlHook.cache_control_as_prompt_cache_breakpoints(
|
||||
chat_completion_prompt_cache_breakpoints_for_wire(message, drop_params)
|
||||
)
|
||||
for message in messages
|
||||
]
|
||||
if reads_breakpoints
|
||||
else [without_prompt_cache_breakpoints(message) for message in messages]
|
||||
if drops_breakpoints
|
||||
|
|
|
|||
|
|
@ -1988,7 +1988,7 @@ class OpenAIResponsesHandler(BaseTranslation):
|
|||
"annotations": (),
|
||||
"logprobs": None,
|
||||
}
|
||||
return (
|
||||
events: Final = (
|
||||
*_open_item_closing_events(responses_so_far),
|
||||
OutputItemAddedEvent(
|
||||
type=ResponsesAPIStreamEvents.OUTPUT_ITEM_ADDED,
|
||||
|
|
@ -2033,6 +2033,10 @@ class OpenAIResponsesHandler(BaseTranslation):
|
|||
response=_blocked_response(exc, response_id=response_id, model=model, output_item=item),
|
||||
),
|
||||
)
|
||||
return tuple(
|
||||
event.model_copy(update={"sequence_number": number})
|
||||
for number, event in enumerate(events, start=_next_stream_sequence_number(responses_so_far))
|
||||
)
|
||||
|
||||
|
||||
class _BlockedContentPart(TypedDict):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from collections.abc import Callable, Collection, Iterable, Mapping, Sequence
|
||||
from functools import lru_cache
|
||||
from functools import lru_cache, partial
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Final, Protocol, cast, get_type_hints
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ import litellm
|
|||
from litellm._logging import verbose_logger
|
||||
from litellm.integrations.anthropic_cache_control_hook import (
|
||||
AnthropicCacheControlHook,
|
||||
openai_prompt_cache_breakpoint_support,
|
||||
deployment_prompt_cache_breakpoint_support,
|
||||
targets_openai_api,
|
||||
)
|
||||
from litellm.litellm_core_utils.core_helpers import normalize_drop_params, process_response_headers
|
||||
|
|
@ -25,6 +25,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
|||
drop_non_python_regex_patterns,
|
||||
flatten_combinators_and_drop_non_python_regex_patterns,
|
||||
prompt_cache_breakpoint_for_wire,
|
||||
whole_prompt_cache_breakpoint_or_none,
|
||||
with_prompt_cache_breakpoint,
|
||||
)
|
||||
from litellm.litellm_core_utils.safe_json_loads import safe_json_loads
|
||||
|
|
@ -103,13 +104,6 @@ _INPUT_ITEM: Final[TypeAdapter[Mapping[str, object]]] = TypeAdapter(Mapping[str,
|
|||
_CONTENT_PARTS: Final[TypeAdapter[Sequence[Mapping[str, object]]]] = TypeAdapter(Sequence[Mapping[str, object]])
|
||||
|
||||
|
||||
class _DeclaredModel(BaseModel):
|
||||
model_config = ConfigDict(frozen=True, extra="ignore", from_attributes=True)
|
||||
|
||||
base_model: object = None
|
||||
model_info: object = None
|
||||
|
||||
|
||||
class _DeploymentHost(BaseModel):
|
||||
"""`responses()` builds the params from raw kwargs, where callers may spell the host `base_url`."""
|
||||
|
||||
|
|
@ -241,12 +235,12 @@ def _json_array_or_empty(value: object) -> Sequence[object]:
|
|||
return value if isinstance(value, (list, tuple)) else ()
|
||||
|
||||
|
||||
def _item_with_wire_safe_prompt_cache_breakpoints(item: object, keeps_markers: bool) -> object:
|
||||
def _item_with_wire_safe_prompt_cache_breakpoints(item: object, to_wire: Callable[[object], object] | None) -> object:
|
||||
fields: Final = _json_object_or_none(item)
|
||||
if fields is None or not _carries_prompt_cache_breakpoint(fields):
|
||||
return item
|
||||
wire_safe_fields: Final = {
|
||||
key: _parts_with_wire_safe_prompt_cache_breakpoints(key, value, keeps_markers)
|
||||
key: _parts_with_wire_safe_prompt_cache_breakpoints(key, value, to_wire)
|
||||
for key, value in fields.items()
|
||||
if key != "prompt_cache_breakpoint"
|
||||
}
|
||||
|
|
@ -270,21 +264,23 @@ def _parts_carry_prompt_cache_breakpoint(parts: object) -> bool:
|
|||
return any(isinstance(part, Mapping) and "prompt_cache_breakpoint" in part for part in _json_array_or_empty(parts))
|
||||
|
||||
|
||||
def _parts_with_wire_safe_prompt_cache_breakpoints(key: str, value: object, keeps_markers: bool) -> object:
|
||||
def _parts_with_wire_safe_prompt_cache_breakpoints(
|
||||
key: str, value: object, to_wire: Callable[[object], object] | None
|
||||
) -> object:
|
||||
if key not in _INPUT_PART_LIST_KEYS or not _parts_carry_prompt_cache_breakpoint(value):
|
||||
return value
|
||||
parts: Final = _json_array_or_empty(value)
|
||||
wire_safe_parts: Final = tuple(_part_with_wire_safe_prompt_cache_breakpoint(part, keeps_markers) for part in parts)
|
||||
wire_safe_parts: Final = tuple(_part_with_wire_safe_prompt_cache_breakpoint(part, to_wire) for part in parts)
|
||||
return value if _same_objects(wire_safe_parts, parts) else list(wire_safe_parts)
|
||||
|
||||
|
||||
def _part_with_wire_safe_prompt_cache_breakpoint(part: object, keeps_markers: bool) -> object:
|
||||
def _part_with_wire_safe_prompt_cache_breakpoint(part: object, to_wire: Callable[[object], object] | None) -> object:
|
||||
fields: Final = _json_object_or_none(part)
|
||||
if fields is None or "prompt_cache_breakpoint" not in fields:
|
||||
return part
|
||||
marker: Final = fields["prompt_cache_breakpoint"]
|
||||
wire_marker: Final = prompt_cache_breakpoint_for_wire(marker, drop_params=True) if keeps_markers else None
|
||||
if keeps_markers and wire_marker == marker:
|
||||
wire_marker: Final = None if to_wire is None else to_wire(marker)
|
||||
if to_wire is not None and wire_marker == marker:
|
||||
return part
|
||||
unmarked: Final = {key: value for key, value in fields.items() if key != "prompt_cache_breakpoint"}
|
||||
return with_prompt_cache_breakpoint(unmarked, wire_marker)
|
||||
|
|
@ -681,24 +677,31 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
|
|||
) -> str | ResponseInputParam:
|
||||
if not isinstance(input, list) or not drops_params(litellm_params):
|
||||
return input
|
||||
keeps_markers: Final = reads_breakpoints or self._keeps_prompt_cache_breakpoints(
|
||||
model=model, litellm_params=litellm_params
|
||||
)
|
||||
wire_safe_items: Final = tuple(
|
||||
_item_with_wire_safe_prompt_cache_breakpoints(item, keeps_markers) for item in input
|
||||
known_support: Final = self._prompt_cache_breakpoint_support(model=model, litellm_params=litellm_params)
|
||||
to_wire: Final = (
|
||||
self._prompt_cache_breakpoint_to_wire(litellm_params, reads_breakpoints, known_support)
|
||||
if reads_breakpoints or known_support is not False
|
||||
else None
|
||||
)
|
||||
wire_safe_items: Final = tuple(_item_with_wire_safe_prompt_cache_breakpoints(item, to_wire) for item in input)
|
||||
if _same_objects(wire_safe_items, input):
|
||||
return input
|
||||
wire_safe_input: Final = list(wire_safe_items)
|
||||
return cast("ResponseInputParam", wire_safe_input) # cast-ok: items keep their shape, minus a rejected marker
|
||||
|
||||
def _keeps_prompt_cache_breakpoints(self, model: str, litellm_params: GenericLiteLLMParams) -> bool:
|
||||
provider: Final = _provider_name(self.custom_llm_provider)
|
||||
base_model: Final = self._declared_base_model(model=model, litellm_params=litellm_params)
|
||||
known_support: Final = (
|
||||
openai_prompt_cache_breakpoint_support(name, provider) for name in dict.fromkeys((base_model, model))
|
||||
def _prompt_cache_breakpoint_to_wire(
|
||||
self, litellm_params: GenericLiteLLMParams, reads_breakpoints: bool, known_support: bool | None
|
||||
) -> Callable[[object], object]:
|
||||
opted_in_openai_host: Final = reads_breakpoints and self.custom_llm_provider == LlmProviders.OPENAI
|
||||
known_openai_reader: Final = known_support is True and self._targets_openai_api(litellm_params)
|
||||
if opted_in_openai_host or known_openai_reader:
|
||||
return partial(prompt_cache_breakpoint_for_wire, drop_params=True)
|
||||
return whole_prompt_cache_breakpoint_or_none
|
||||
|
||||
def _prompt_cache_breakpoint_support(self, model: str, litellm_params: GenericLiteLLMParams) -> bool | None:
|
||||
return deployment_prompt_cache_breakpoint_support(
|
||||
model, _provider_name(self.custom_llm_provider), litellm_params
|
||||
)
|
||||
return next((support for support in known_support if support is not None), None) is not False
|
||||
|
||||
def _targets_openai_api(self, litellm_params: GenericLiteLLMParams) -> bool:
|
||||
host: Final = _DeploymentHost.model_validate(litellm_params)
|
||||
|
|
@ -872,13 +875,6 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
|
|||
base_model: Final[object] = model_info.get("base_model") if isinstance(model_info, dict) else None
|
||||
return base_model if isinstance(base_model, str) and base_model else model
|
||||
|
||||
@staticmethod
|
||||
def _declared_base_model(model: str, litellm_params: GenericLiteLLMParams) -> str:
|
||||
declared: Final = _DeclaredModel.model_validate(litellm_params)
|
||||
model_info: Final = _json_object_or_none(declared.model_info)
|
||||
candidates: Final = (declared.base_model, None if model_info is None else model_info.get("base_model"))
|
||||
return next((name for name in candidates if isinstance(name, str) and name), model)
|
||||
|
||||
@staticmethod
|
||||
def _sanitized_tool_entry(
|
||||
entry: Mapping[str, object],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Final
|
||||
from typing import TYPE_CHECKING, ClassVar, Final
|
||||
|
||||
import httpx
|
||||
from pydantic import TypeAdapter
|
||||
|
|
@ -159,6 +159,7 @@ class OpenCodeGoMessagesChatConfig(_GoReportedCostMixin, AnthropicConfig):
|
|||
|
||||
class OpenCodeGoGeminiChatConfig(_GoReportedCostMixin, GoogleAIStudioGeminiConfig):
|
||||
go_usage_key: str = "usageMetadata"
|
||||
_route: ClassVar[str] = "opencode_go"
|
||||
|
||||
@property
|
||||
def supports_stream_param_in_request_body(self) -> bool:
|
||||
|
|
|
|||
|
|
@ -1458,6 +1458,7 @@ class VertexAITokenCounter(BaseTokenCounter):
|
|||
) -> TokenCountResponse | None:
|
||||
import copy
|
||||
|
||||
from litellm.llms.anthropic.common_utils import context_management_with_edits
|
||||
from litellm.llms.vertex_ai.vertex_ai_partner_models.count_tokens.handler import count_tokens_publisher
|
||||
from litellm.llms.vertex_ai.vertex_ai_partner_models.main import (
|
||||
VertexAIError as PartnerVertexAIError,
|
||||
|
|
@ -1539,6 +1540,9 @@ class VertexAITokenCounter(BaseTokenCounter):
|
|||
system=system,
|
||||
tools=tools,
|
||||
count_options=count_options,
|
||||
context_management=(
|
||||
None if context_management is None else context_management_with_edits(context_management.spec)
|
||||
),
|
||||
)
|
||||
except (PartnerVertexAIError, httpx.HTTPStatusError) as e:
|
||||
status_code: Final = e.response.status_code
|
||||
|
|
@ -1565,12 +1569,15 @@ class VertexAITokenCounter(BaseTokenCounter):
|
|||
original_response=result,
|
||||
)
|
||||
else:
|
||||
from litellm.llms.anthropic.messages.context_management.inference_view import (
|
||||
InferenceRequestViewError,
|
||||
count_context_edits,
|
||||
inference_request_view,
|
||||
)
|
||||
from litellm.llms.vertex_ai.count_tokens.context import (
|
||||
VertexGeminiCountContextError,
|
||||
count_context_edits,
|
||||
count_context_error_response,
|
||||
gemini_count_contents,
|
||||
inference_request_view,
|
||||
malformed_tool_result_text_count_error,
|
||||
provider_error_response,
|
||||
unconvertible_messages_error,
|
||||
|
|
@ -1584,7 +1591,7 @@ class VertexAITokenCounter(BaseTokenCounter):
|
|||
messages_format,
|
||||
count_context_edits(context_management, tools, count_tokens_params_request, model_to_use),
|
||||
)
|
||||
if isinstance(request_view, VertexGeminiCountContextError):
|
||||
if isinstance(request_view, InferenceRequestViewError):
|
||||
return count_context_error_response(request_view, request_model, model_to_use)
|
||||
count_context: Final = vertex_gemini_count_context(
|
||||
system=request_view.system, tools=tools, model=model_to_use
|
||||
|
|
|
|||
|
|
@ -11,10 +11,11 @@ from litellm.litellm_core_utils.prompt_templates.factory import chat_completion_
|
|||
from litellm.types.llms.anthropic import AnthropicMessagesTextParam
|
||||
from litellm.types.llms.openai import AllMessageValues
|
||||
from litellm.types.llms.vertex_ai import ContentType
|
||||
from litellm.types.utils import CountContextManagement, TokenCountMessagesFormat, TokenCountResponse
|
||||
from litellm.types.utils import TokenCountMessagesFormat, TokenCountResponse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.exceptions import APIError
|
||||
from litellm.llms.anthropic.messages.context_management.inference_view import InferenceRequestViewError
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
|
@ -152,7 +153,7 @@ def malformed_tool_result_text_count_error(
|
|||
|
||||
|
||||
def count_context_error_response(
|
||||
error: VertexGeminiCountContextError, request_model: str, model_used: str
|
||||
error: "VertexGeminiCountContextError | InferenceRequestViewError", request_model: str, model_used: str
|
||||
) -> TokenCountResponse:
|
||||
return TokenCountResponse(
|
||||
total_tokens=0,
|
||||
|
|
@ -178,130 +179,6 @@ def provider_error_response(error: "APIError", request_model: str, model_used: s
|
|||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class InferenceRequestView:
|
||||
messages: Sequence[Mapping[str, object]]
|
||||
system: object | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CountContextEdits:
|
||||
spec: JsonValue
|
||||
model: str
|
||||
tools: tuple[Mapping[str, object], ...] | None
|
||||
drop_invalid_edits: bool = False
|
||||
|
||||
|
||||
_CONTEXT_MANAGEMENT_SPEC: Final[TypeAdapter[Mapping[str, object] | tuple[Mapping[str, object], ...]]] = TypeAdapter(
|
||||
Mapping[str, object] | tuple[Mapping[str, object], ...]
|
||||
)
|
||||
|
||||
|
||||
def count_context_edits(
|
||||
context_management: CountContextManagement | None,
|
||||
tools: Sequence[Mapping[str, object]] | None,
|
||||
litellm_params: Mapping[str, object],
|
||||
fallback_model: str,
|
||||
) -> CountContextEdits:
|
||||
model: Final = litellm_params.get("model")
|
||||
return CountContextEdits(
|
||||
spec=None if context_management is None else context_management.spec,
|
||||
model=model if isinstance(model, str) else fallback_model,
|
||||
tools=None if tools is None else tuple(tools),
|
||||
drop_invalid_edits=context_management is not None and context_management.drop_invalid_edits,
|
||||
)
|
||||
|
||||
|
||||
def _typed_spec(spec: JsonValue) -> Mapping[str, object] | tuple[Mapping[str, object], ...] | None:
|
||||
try:
|
||||
return _CONTEXT_MANAGEMENT_SPEC.validate_python(spec)
|
||||
except ValidationError:
|
||||
return None
|
||||
|
||||
|
||||
def _typed_system(
|
||||
system: object | None,
|
||||
) -> str | tuple[AnthropicMessagesTextParam, ...] | None | VertexGeminiCountContextError:
|
||||
if system is None or isinstance(system, str):
|
||||
return system
|
||||
try:
|
||||
return _SYSTEM_BLOCKS.validate_python(system)
|
||||
except ValidationError:
|
||||
return _INVALID_SYSTEM
|
||||
|
||||
|
||||
def _view_without_edits(
|
||||
messages: Sequence[Mapping[str, object]],
|
||||
system: object | None,
|
||||
typed_system: str | Sequence[Mapping[str, object]] | None,
|
||||
) -> InferenceRequestView:
|
||||
from litellm.llms.anthropic.messages.context_management.editors.compact import (
|
||||
apply_client_compaction_block_history,
|
||||
)
|
||||
|
||||
history: Final = apply_client_compaction_block_history(
|
||||
messages=[dict(message) for message in messages],
|
||||
system=typed_system
|
||||
if typed_system is None or isinstance(typed_system, str)
|
||||
else [dict(block) for block in typed_system],
|
||||
)
|
||||
if history is None:
|
||||
return InferenceRequestView(messages=messages, system=system)
|
||||
return InferenceRequestView(messages=history.messages, system=history.system)
|
||||
|
||||
|
||||
async def _anthropic_inference_view(
|
||||
messages: Sequence[Mapping[str, object]], system: object | None, edits: CountContextEdits
|
||||
) -> InferenceRequestView | VertexGeminiCountContextError:
|
||||
from litellm.llms.anthropic.messages.adapters.handler import prepare_context_managed_request
|
||||
from litellm.llms.anthropic.messages.context_management import AnthropicContextManagementError
|
||||
from litellm.llms.anthropic.messages.native.utils import normalize_native_messages
|
||||
|
||||
typed_system: Final = _typed_system(system)
|
||||
if isinstance(typed_system, VertexGeminiCountContextError):
|
||||
return typed_system
|
||||
normalized: Final = normalize_native_messages(messages)
|
||||
spec: Final = _typed_spec(edits.spec)
|
||||
try:
|
||||
prepared: Final = await prepare_context_managed_request(
|
||||
model=edits.model,
|
||||
messages=normalized,
|
||||
tools=None if edits.tools is None else [dict(tool) for tool in edits.tools],
|
||||
system=[dict(block) for block in typed_system] if isinstance(typed_system, tuple) else typed_system,
|
||||
context_management_spec=(
|
||||
None if spec is None else dict(spec) if isinstance(spec, Mapping) else [dict(edit) for edit in spec]
|
||||
),
|
||||
litellm_metadata=None,
|
||||
additional_drop_params=None,
|
||||
llm_router=None,
|
||||
count_only=True,
|
||||
drop_invalid_edits=edits.drop_invalid_edits,
|
||||
)
|
||||
except AnthropicContextManagementError as error:
|
||||
if error.status_code >= 500:
|
||||
return _view_without_edits(normalized, system, typed_system)
|
||||
return VertexGeminiCountContextError(error_message=error.message, status_code=error.status_code)
|
||||
if prepared is None:
|
||||
return InferenceRequestView(messages=normalized, system=system)
|
||||
return InferenceRequestView(messages=prepared.messages, system=prepared.system)
|
||||
|
||||
|
||||
async def inference_request_view(
|
||||
messages: Sequence[Mapping[str, object]],
|
||||
system: object | None,
|
||||
messages_format: TokenCountMessagesFormat,
|
||||
edits: CountContextEdits,
|
||||
) -> InferenceRequestView | VertexGeminiCountContextError:
|
||||
"""Apply the history rewrites and context edits /v1/messages runs before it converts a body for Gemini."""
|
||||
match messages_format:
|
||||
case "openai":
|
||||
return InferenceRequestView(messages=messages, system=system)
|
||||
case "anthropic":
|
||||
return await _anthropic_inference_view(messages, system, edits)
|
||||
case _:
|
||||
assert_never(messages_format)
|
||||
|
||||
|
||||
def _chat_messages(
|
||||
messages: Sequence[Mapping[str, object]],
|
||||
messages_format: TokenCountMessagesFormat,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|||
from copy import deepcopy
|
||||
from functools import lru_cache, partial
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, Optional, Protocol, Union, cast, get_args
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Final, Literal, Optional, Protocol, Union, cast, get_args
|
||||
|
||||
import httpx
|
||||
from pydantic import JsonValue, TypeAdapter
|
||||
|
|
@ -34,6 +34,7 @@ from litellm.litellm_core_utils.json_fragment_accumulator import JSONFragmentAcc
|
|||
from litellm.litellm_core_utils.model_param_helper import embeds_thought_signature_in_tool_call_id
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import encode_tool_call_id_with_signature
|
||||
from litellm.litellm_core_utils.reasoning_effort_utils import warn_dropped_reasoning
|
||||
from litellm.litellm_core_utils.tool_fields import tool_params_without_hosted_mcp
|
||||
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
|
||||
from litellm.llms.custom_httpx.http_handler import (
|
||||
AsyncHTTPHandler,
|
||||
|
|
@ -437,6 +438,8 @@ _NAMED_UNKNOWN_VOICE_KEYS: Final = 10
|
|||
|
||||
_NAMED_VOICE_KEY_CHARS: Final = 100
|
||||
|
||||
_NAMED_AUDIO_FORMAT_CHARS: Final = 100
|
||||
|
||||
|
||||
def _gemini_speech_config_wrapper(value: Mapping[str, object]) -> Mapping[str, object] | None:
|
||||
camel_config: Final = as_str_mapping(value.get("speechConfig"))
|
||||
|
|
@ -473,6 +476,31 @@ def reject_unknown_gemini_tts_voice_keys(
|
|||
)
|
||||
|
||||
|
||||
def _named_audio_format(audio_format: object) -> str:
|
||||
if isinstance(audio_format, str):
|
||||
return repr(audio_format[:_NAMED_AUDIO_FORMAT_CHARS])
|
||||
return repr(audio_format)[:_NAMED_AUDIO_FORMAT_CHARS]
|
||||
|
||||
|
||||
def unsupported_gemini_tts_audio_format(
|
||||
model: str, audio: Mapping[str, object], drop_params: bool, llm_provider: str | None
|
||||
) -> UnsupportedParamsError | None:
|
||||
nested_config: Final = _gemini_speech_config_wrapper(audio) or {}
|
||||
audio_format: Final = next(
|
||||
(item for item in (audio.get("format"), nested_config.get("format")) if item not in (None, "pcm16")), None
|
||||
)
|
||||
if audio_format is None or drops_unsupported_params(drop_params):
|
||||
return None
|
||||
return UnsupportedParamsError(
|
||||
message=(
|
||||
f"{model} does not take the audio format {_named_audio_format(audio_format)}. Set format "
|
||||
"to 'pcm16' or leave it out, or set drop_params to drop it."
|
||||
),
|
||||
model=model,
|
||||
llm_provider=llm_provider,
|
||||
)
|
||||
|
||||
|
||||
def normalize_gemini_speech_config(
|
||||
value: Mapping[str, object], drop_unknown_fields: bool = False
|
||||
) -> dict[str, object]: # mutable-ok: provider request serialization requires a concrete dict
|
||||
|
|
@ -587,6 +615,8 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
Note: Please make sure to modify the default parameters as required for your use case.
|
||||
"""
|
||||
|
||||
_route: ClassVar[str] = "vertex_ai"
|
||||
|
||||
temperature: float | None = None
|
||||
max_output_tokens: int | None = None
|
||||
top_p: float | None = None
|
||||
|
|
@ -1555,15 +1585,11 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
languageCode: "en-US",
|
||||
}
|
||||
"""
|
||||
# Validate audio format - Gemini TTS only supports pcm16
|
||||
audio_format: Final = value.get("format")
|
||||
if audio_format is not None and audio_format != "pcm16":
|
||||
raise ValueError(
|
||||
f"Unsupported audio format for Gemini TTS models: {audio_format}. "
|
||||
f"Gemini TTS models only support 'pcm16' format as they return audio data in L16 PCM format. "
|
||||
f"Please set audio format to 'pcm16'."
|
||||
)
|
||||
|
||||
audio_format_error: Final = unsupported_gemini_tts_audio_format(
|
||||
"Gemini TTS", value, drop_unknown_fields, self.custom_llm_provider
|
||||
)
|
||||
if audio_format_error is not None:
|
||||
raise audio_format_error
|
||||
camel_speech_config: Final = value.get("speechConfig")
|
||||
snake_speech_config: Final = value.get("speech_config")
|
||||
speech_config_value: Final = (
|
||||
|
|
@ -1601,6 +1627,11 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
def _map_chat_audio_params(
|
||||
self, model: str, value: Mapping[str, object], drop_params: bool
|
||||
) -> dict[str, object]: # mutable-ok: the public forwarding interface returns a concrete request dictionary
|
||||
audio_format_error: Final = unsupported_gemini_tts_audio_format(
|
||||
model, value, drop_params, self.custom_llm_provider
|
||||
)
|
||||
if audio_format_error is not None:
|
||||
raise audio_format_error
|
||||
if _gemini_speech_config_wrapper(value) is not None:
|
||||
reject_unknown_gemini_tts_voice_keys(model, value, drop_params, self.custom_llm_provider)
|
||||
return self._map_audio_params(value, drop_unknown_fields=drops_unsupported_params(drop_params))
|
||||
|
|
@ -1639,9 +1670,12 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
drop_params: bool,
|
||||
allow_integer_enums: bool = True,
|
||||
) -> dict:
|
||||
self._apply_include_server_side_tool_invocations(non_default_params, optional_params)
|
||||
request_params: Final = tool_params_without_hosted_mcp(
|
||||
non_default_params, model=model, route=self._route, drop_params=drop_params
|
||||
)
|
||||
self._apply_include_server_side_tool_invocations(request_params, optional_params)
|
||||
supports_sampling_params: Final = self.supports_sampling_params(model)
|
||||
for param, value in non_default_params.items():
|
||||
for param, value in request_params.items():
|
||||
if param in ("temperature", "top_p", "top_k") and not supports_sampling_params:
|
||||
continue
|
||||
if param == "temperature":
|
||||
|
|
@ -1705,7 +1739,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
if _tool_choice_value is not None:
|
||||
optional_params["tool_choice"] = _tool_choice_value
|
||||
elif param == "parallel_tool_calls":
|
||||
tools_list = non_default_params.get("tools", non_default_params.get("functions"))
|
||||
tools_list = request_params.get("tools", request_params.get("functions"))
|
||||
num_tools = len(tools_list) if isinstance(tools_list, list) else 0
|
||||
# Gemini does not support parallel_tool_calls=False with multiple
|
||||
# tools. Drop the param instead of failing — Responses API clients
|
||||
|
|
|
|||
|
|
@ -9,12 +9,14 @@ their respective publisher-specific count-tokens endpoints.
|
|||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any, Final
|
||||
|
||||
from pydantic import TypeAdapter
|
||||
from pydantic import JsonValue, TypeAdapter
|
||||
|
||||
from litellm.anthropic_beta_headers_manager import aload_beta_headers_config, filter_and_transform_beta_headers
|
||||
from litellm.litellm_core_utils.core_helpers import drops_unsupported_params
|
||||
from litellm.litellm_core_utils.url_utils import join_api_base
|
||||
from litellm.llms.anthropic.common_utils import (
|
||||
OBJECT_LIST_ADAPTER,
|
||||
context_management_betas,
|
||||
count_tokens_feature_betas,
|
||||
is_claude_model_target,
|
||||
tools_without_custom_field,
|
||||
|
|
@ -31,6 +33,7 @@ _COUNT_TOKENS_PUBLISHERS: Final = (
|
|||
|
||||
|
||||
_REQUEST_MESSAGES: Final = TypeAdapter(Sequence[object])
|
||||
_CONTEXT_MANAGEMENT: Final[TypeAdapter[JsonValue]] = TypeAdapter(JsonValue)
|
||||
|
||||
|
||||
def count_tokens_request_body(
|
||||
|
|
@ -55,8 +58,14 @@ def count_tokens_publisher(model: str) -> str | None:
|
|||
|
||||
def count_tokens_headers(access_token: str, request_data: Mapping[str, object]) -> Mapping[str, str]:
|
||||
authorization: Final = {"Authorization": f"Bearer {access_token}"}
|
||||
betas: Final = count_tokens_feature_betas(
|
||||
"vertex_ai", request_data, _REQUEST_MESSAGES.validate_python(request_data["messages"])
|
||||
betas: Final = (
|
||||
*count_tokens_feature_betas(
|
||||
"vertex_ai", request_data, _REQUEST_MESSAGES.validate_python(request_data["messages"])
|
||||
),
|
||||
*filter_and_transform_beta_headers(
|
||||
list(context_management_betas(_CONTEXT_MANAGEMENT.validate_python(request_data.get("context_management")))),
|
||||
"vertex_ai",
|
||||
),
|
||||
)
|
||||
return {**authorization, "anthropic-beta": ",".join(betas)} if betas else authorization
|
||||
|
||||
|
|
@ -193,6 +202,8 @@ class VertexAIPartnerModelsTokenCounter(VertexBase):
|
|||
api_base=litellm_params.get("api_base"),
|
||||
)
|
||||
|
||||
if request_data.get("context_management") is not None:
|
||||
await aload_beta_headers_config()
|
||||
headers: Final = dict(count_tokens_headers(access_token, request_data))
|
||||
|
||||
# Get async HTTP client
|
||||
|
|
|
|||
|
|
@ -268,6 +268,7 @@ class VertexAIPartnerModels(VertexBase):
|
|||
system: object | None = None,
|
||||
tools: list[dict[str, object]] | None = None,
|
||||
count_options: Mapping[str, object] = MappingProxyType({}),
|
||||
context_management: object = None,
|
||||
):
|
||||
"""
|
||||
Count tokens for Vertex AI partner models (Anthropic Claude, Mistral, etc.)
|
||||
|
|
@ -302,7 +303,15 @@ class VertexAIPartnerModels(VertexBase):
|
|||
"model": model,
|
||||
"messages": messages,
|
||||
**MappingProxyType(
|
||||
{key: value for key, value in (("system", system), ("tools", tools)) if value is not None}
|
||||
{
|
||||
key: value
|
||||
for key, value in (
|
||||
("system", system),
|
||||
("tools", tools),
|
||||
("context_management", context_management),
|
||||
)
|
||||
if value is not None
|
||||
}
|
||||
),
|
||||
**count_options,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,9 +75,7 @@ def _public_request(
|
|||
return native_call(legacy, args, _native_kwargs(fields, kwargs))
|
||||
|
||||
|
||||
def _resolved(request: NativeCall) -> tuple[str, str | None]:
|
||||
model: Final = str(request.resolved["model"])
|
||||
provider: Final = optional_str(request.resolved.get("custom_llm_provider"))
|
||||
def _resolved_model(model: str, provider: str | None) -> tuple[str, str | None]:
|
||||
try:
|
||||
resolved_model, resolved_provider, _, _ = get_llm_provider(model, provider)
|
||||
except BadRequestError:
|
||||
|
|
@ -85,21 +83,43 @@ def _resolved(request: NativeCall) -> tuple[str, str | None]:
|
|||
return resolved_model, resolved_provider
|
||||
|
||||
|
||||
def _target(request: NativeCall, model: str, provider: str | None) -> Target:
|
||||
requested_model: Final = str(request.resolved["model"])
|
||||
model_info: Final = request.resolved.get("model_info")
|
||||
api_base: Final = request.resolved.get("api_base")
|
||||
native_config: Final = main._native_messages_config( # pyright: ignore[reportPrivateUsage] # the handler's own passthrough lookup
|
||||
model, provider, model_info, api_base if isinstance(api_base, str) else None
|
||||
)
|
||||
def _resolved(request: NativeCall) -> tuple[str, str | None]:
|
||||
return _resolved_model(str(request.resolved["model"]), optional_str(request.resolved.get("custom_llm_provider")))
|
||||
|
||||
|
||||
def _route_target(
|
||||
requested_model: str,
|
||||
model: str,
|
||||
provider: str | None,
|
||||
model_info: object,
|
||||
api_base: str | None,
|
||||
compacting: bool,
|
||||
) -> Target:
|
||||
native_config: Final = main._native_messages_config(model, provider, model_info, api_base) # pyright: ignore[reportPrivateUsage] # the handler's own passthrough lookup
|
||||
if native_config is not None:
|
||||
return Target.MESSAGES
|
||||
compacting: Final = request.resolved.get("compaction") is not None
|
||||
if not compacting and main._should_route_to_responses_api(provider, requested_model, model, model_info): # pyright: ignore[reportPrivateUsage] # the handler's own translation choice
|
||||
return Target.RESPONSES
|
||||
return Target.CHAT_COMPLETIONS
|
||||
|
||||
|
||||
def _target(request: NativeCall, model: str, provider: str | None) -> Target:
|
||||
api_base: Final = request.resolved.get("api_base")
|
||||
return _route_target(
|
||||
str(request.resolved["model"]),
|
||||
model,
|
||||
provider,
|
||||
request.resolved.get("model_info"),
|
||||
api_base if isinstance(api_base, str) else None,
|
||||
compacting=request.resolved.get("compaction") is not None,
|
||||
)
|
||||
|
||||
|
||||
def messages_target(model: str, custom_llm_provider: str | None, model_info: object, api_base: str | None) -> Target:
|
||||
resolved_model, resolved_provider = _resolved_model(model, custom_llm_provider)
|
||||
return _route_target(model, resolved_model, resolved_provider, model_info, api_base, compacting=False)
|
||||
|
||||
|
||||
_FEATURES: Final[detectors.Detectors] = MappingProxyType(
|
||||
{
|
||||
**detectors.COMMON,
|
||||
|
|
|
|||
|
|
@ -13478,6 +13478,10 @@
|
|||
"input_cost_per_token": 2e-07,
|
||||
"output_cost_per_token": 5e-07,
|
||||
"litellm_provider": "azure_ai",
|
||||
"token_count_framing": 149,
|
||||
"token_count_tool_framing": 302,
|
||||
"token_count_responses_framing": 145,
|
||||
"token_count_responses_tool_framing": 100,
|
||||
"max_input_tokens": 128000,
|
||||
"max_output_tokens": 128000,
|
||||
"mode": "chat",
|
||||
|
|
@ -15555,18 +15559,20 @@
|
|||
"input_cost_per_token": 2.65e-06,
|
||||
"litellm_provider": "bedrock",
|
||||
"max_input_tokens": 8000,
|
||||
"max_output_tokens": 2048,
|
||||
"max_output_tokens": 8192,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 3.5e-06,
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-meta-llama-3-70b-instruct.md",
|
||||
"supports_pdf_input": true
|
||||
},
|
||||
"bedrock/us-gov-west-1/meta.llama3-8b-instruct-v1:0": {
|
||||
"input_cost_per_token": 3e-07,
|
||||
"litellm_provider": "bedrock",
|
||||
"max_input_tokens": 8000,
|
||||
"max_output_tokens": 2048,
|
||||
"max_output_tokens": 8192,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 6e-07,
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-meta-llama-3-8b-instruct.md",
|
||||
"supports_pdf_input": true
|
||||
},
|
||||
"bedrock/us-west-1/meta.llama3-70b-instruct-v1:0": {
|
||||
|
|
@ -48955,6 +48961,7 @@
|
|||
},
|
||||
"together_ai/Qwen/Qwen3.8-2.4T-A95B": {
|
||||
"cache_read_input_token_cost": 2.5e-07,
|
||||
"deprecation_date": "2026-11-04",
|
||||
"input_cost_per_token": 2e-06,
|
||||
"litellm_provider": "together_ai",
|
||||
"max_input_tokens": 1010000,
|
||||
|
|
@ -53954,23 +53961,23 @@
|
|||
"supports_vision": true
|
||||
},
|
||||
"wandb/meta-llama/Llama-3.1-8B-Instruct": {
|
||||
"max_input_tokens": 131000,
|
||||
"max_input_tokens": 128000,
|
||||
"max_output_tokens": 128000,
|
||||
"input_cost_per_token": 2.2e-07,
|
||||
"output_cost_per_token": 2.2e-07,
|
||||
"litellm_provider": "wandb",
|
||||
"mode": "chat",
|
||||
"source": "https://wandb.ai/site/pricing/tokens/"
|
||||
"source": "https://www.coreweave.com/products/coreweave-forge/serverless-inference"
|
||||
},
|
||||
"wandb/deepseek-ai/DeepSeek-V3.1": {
|
||||
"supports_reasoning": true,
|
||||
"max_input_tokens": 161000,
|
||||
"max_input_tokens": 128000,
|
||||
"max_output_tokens": 128000,
|
||||
"input_cost_per_token": 5.5e-07,
|
||||
"output_cost_per_token": 1.65e-06,
|
||||
"litellm_provider": "wandb",
|
||||
"mode": "chat",
|
||||
"source": "https://wandb.ai/site/pricing/tokens/"
|
||||
"source": "https://www.coreweave.com/products/coreweave-forge/serverless-inference"
|
||||
},
|
||||
"wandb/meta-llama/Llama-3.3-70B-Instruct": {
|
||||
"max_input_tokens": 128000,
|
||||
|
|
@ -54454,6 +54461,8 @@
|
|||
"input_cost_per_token_above_200k_tokens_batches": 2e-06,
|
||||
"input_cost_per_token_batches": 1e-06,
|
||||
"litellm_provider": "xai",
|
||||
"token_count_framing": 185,
|
||||
"token_count_tool_framing": 214,
|
||||
"max_input_tokens": 1000000,
|
||||
"max_output_tokens": 1000000,
|
||||
"mode": "chat",
|
||||
|
|
@ -59872,6 +59881,7 @@
|
|||
"max_input_tokens": 922000,
|
||||
"max_output_tokens": 128000,
|
||||
"mode": "responses",
|
||||
"search_context_cost_per_query": {"search_context_size_high":0.012,"search_context_size_low":0.012,"search_context_size_medium":0.012},
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-cyber.html",
|
||||
"use_openai_responses_path": true,
|
||||
"supported_endpoints": [
|
||||
|
|
@ -59893,7 +59903,8 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_xhigh_reasoning_effort": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true
|
||||
},
|
||||
"bedrock_mantle/openai.gpt-daybreak-blue-5.6-sol": {
|
||||
"input_cost_per_token": 4.4e-06,
|
||||
|
|
@ -59908,6 +59919,7 @@
|
|||
"max_input_tokens": 1050000,
|
||||
"max_output_tokens": 128000,
|
||||
"mode": "responses",
|
||||
"search_context_cost_per_query": {"search_context_size_high":0.012,"search_context_size_low":0.012,"search_context_size_medium":0.012},
|
||||
"use_openai_responses_path": true,
|
||||
"supported_endpoints": [
|
||||
"/v1/responses"
|
||||
|
|
@ -59927,7 +59939,8 @@
|
|||
"supports_tool_choice": true,
|
||||
"supports_xhigh_reasoning_effort": true,
|
||||
"supports_vision": true,
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-daybreak-blue-56-sol.html"
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-daybreak-blue-56-sol.html",
|
||||
"supports_web_search": true
|
||||
},
|
||||
"bedrock_mantle/openai.gpt-5.6-luna": {
|
||||
"input_cost_per_token": 2.2e-07,
|
||||
|
|
@ -60333,6 +60346,7 @@
|
|||
"mode": "responses",
|
||||
"output_cost_per_token_above_272k_tokens_ultrafast": 0.000495,
|
||||
"output_cost_per_token_ultrafast": 0.00033,
|
||||
"search_context_cost_per_query": {"search_context_size_high":0.012,"search_context_size_low":0.012,"search_context_size_medium":0.012},
|
||||
"use_openai_responses_path": true,
|
||||
"supported_endpoints": [
|
||||
"/v1/chat/completions",
|
||||
|
|
@ -60356,7 +60370,8 @@
|
|||
"supports_tool_choice": true,
|
||||
"supports_xhigh_reasoning_effort": true,
|
||||
"supports_vision": true,
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-6-astra.html"
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-6-astra.html",
|
||||
"supports_web_search": true
|
||||
},
|
||||
"bedrock_mantle/openai.gpt-6-sol": {
|
||||
"input_cost_per_token": 2.2e-06,
|
||||
|
|
@ -60371,6 +60386,7 @@
|
|||
"max_input_tokens": 922000,
|
||||
"max_output_tokens": 128000,
|
||||
"mode": "responses",
|
||||
"search_context_cost_per_query": {"search_context_size_high":0.012,"search_context_size_low":0.012,"search_context_size_medium":0.012},
|
||||
"use_openai_responses_path": true,
|
||||
"supported_endpoints": [
|
||||
"/v1/chat/completions",
|
||||
|
|
@ -60394,7 +60410,8 @@
|
|||
"supports_tool_choice": true,
|
||||
"supports_xhigh_reasoning_effort": true,
|
||||
"supports_vision": true,
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-cards-openai.html"
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-cards-openai.html",
|
||||
"supports_web_search": true
|
||||
},
|
||||
"bedrock_mantle/openai.gpt-6-luna": {
|
||||
"input_cost_per_token": 1.1e-07,
|
||||
|
|
@ -60409,6 +60426,7 @@
|
|||
"max_input_tokens": 922000,
|
||||
"max_output_tokens": 128000,
|
||||
"mode": "responses",
|
||||
"search_context_cost_per_query": {"search_context_size_high":0.012,"search_context_size_low":0.012,"search_context_size_medium":0.012},
|
||||
"use_openai_responses_path": true,
|
||||
"supported_endpoints": [
|
||||
"/v1/chat/completions",
|
||||
|
|
@ -60432,7 +60450,8 @@
|
|||
"supports_tool_choice": true,
|
||||
"supports_xhigh_reasoning_effort": true,
|
||||
"supports_vision": true,
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-cards-openai.html"
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-cards-openai.html",
|
||||
"supports_web_search": true
|
||||
},
|
||||
"us.openai.gpt-6-astra": {
|
||||
"supports_bedrock_runtime_chat_completions_response_format": true,
|
||||
|
|
@ -62627,7 +62646,9 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"token_count_framing": 23,
|
||||
"token_count_tool_framing": 12
|
||||
},
|
||||
"deepseek/deepseek-v4-flash-vision-exp": {
|
||||
"cache_creation_input_token_cost": 0.0,
|
||||
|
|
@ -62733,7 +62754,9 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": false
|
||||
"supports_vision": false,
|
||||
"token_count_framing": 76,
|
||||
"token_count_tool_framing": 75
|
||||
},
|
||||
"tencent/deepseek-v4-pro": {
|
||||
"cache_creation_input_token_cost": 0.0,
|
||||
|
|
@ -64019,6 +64042,7 @@
|
|||
"input_cost_per_token": 3e-07,
|
||||
"input_cost_per_token_priority": 3.75e-07,
|
||||
"litellm_provider": "fireworks_ai",
|
||||
"token_count_framing": 23,
|
||||
"max_input_tokens": 1048576,
|
||||
"max_output_tokens": 393216,
|
||||
"mode": "chat",
|
||||
|
|
@ -64026,6 +64050,7 @@
|
|||
"output_cost_per_token_priority": 1.5e-06,
|
||||
"source": "https://docs.fireworks.ai/serverless/pricing",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
|
|
@ -64045,6 +64070,7 @@
|
|||
"output_cost_per_token_priority": 2.25e-06,
|
||||
"source": "https://docs.fireworks.ai/serverless/pricing",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
|
|
@ -64087,6 +64113,7 @@
|
|||
],
|
||||
"source": "https://api.fireworks.ai/v1/serverless/models",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
@ -64117,6 +64144,7 @@
|
|||
"input_cost_per_token": 3e-07,
|
||||
"input_cost_per_token_priority": 3.75e-07,
|
||||
"litellm_provider": "fireworks_ai",
|
||||
"token_count_framing": 23,
|
||||
"max_input_tokens": 1048576,
|
||||
"max_output_tokens": 393216,
|
||||
"mode": "chat",
|
||||
|
|
@ -64124,6 +64152,7 @@
|
|||
"output_cost_per_token_priority": 1.5e-06,
|
||||
"source": "https://docs.fireworks.ai/serverless/pricing",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
|
|
@ -64143,6 +64172,7 @@
|
|||
"output_cost_per_token_priority": 2.25e-06,
|
||||
"source": "https://docs.fireworks.ai/serverless/pricing",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
|
|
@ -64217,6 +64247,7 @@
|
|||
],
|
||||
"source": "https://api.fireworks.ai/v1/serverless/models",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
@ -64237,6 +64268,7 @@
|
|||
],
|
||||
"source": "https://docs.fireworks.ai/serverless/pricing",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
@ -64260,6 +64292,7 @@
|
|||
],
|
||||
"source": "https://docs.fireworks.ai/serverless/pricing",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
@ -64335,6 +64368,7 @@
|
|||
"output_cost_per_token_priority": 3e-06,
|
||||
"source": "https://api.fireworks.ai/v1/serverless/models?format=nested",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
@ -64371,6 +64405,7 @@
|
|||
"output_cost_per_token_priority": 6.375e-06,
|
||||
"source": "https://docs.fireworks.ai/serverless/pricing",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_vision": true
|
||||
},
|
||||
|
|
@ -64407,6 +64442,7 @@
|
|||
"output_cost_per_token_priority": 3e-06,
|
||||
"source": "https://api.fireworks.ai/v1/serverless/models?format=nested",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
@ -64477,6 +64513,7 @@
|
|||
],
|
||||
"source": "https://api.fireworks.ai/v1/serverless/models",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
@ -64500,6 +64537,7 @@
|
|||
],
|
||||
"source": "https://docs.fireworks.ai/serverless/pricing",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
@ -67293,6 +67331,7 @@
|
|||
"output_cost_per_token_priority": 5.5e-06,
|
||||
"source": "https://api.fireworks.ai/v1/serverless/models",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
@ -67311,6 +67350,7 @@
|
|||
"output_cost_per_token_priority": 8.25e-06,
|
||||
"source": "https://docs.fireworks.ai/serverless/pricing",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
@ -67329,6 +67369,7 @@
|
|||
"output_cost_per_token_priority": 5.5e-06,
|
||||
"source": "https://api.fireworks.ai/v1/serverless/models",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
@ -67347,6 +67388,7 @@
|
|||
"output_cost_per_token_priority": 8.25e-06,
|
||||
"source": "https://docs.fireworks.ai/serverless/pricing",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
@ -67362,6 +67404,7 @@
|
|||
"output_cost_per_token": 6.6e-06,
|
||||
"source": "https://api.fireworks.ai/v1/serverless/models",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
@ -67404,6 +67447,7 @@
|
|||
"output_cost_per_token": 6.6e-06,
|
||||
"source": "https://docs.fireworks.ai/serverless/pricing",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
@ -67422,6 +67466,7 @@
|
|||
"output_cost_per_token_priority": 6.25e-07,
|
||||
"source": "https://api.fireworks.ai/v1/serverless/models",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
|
|
@ -67440,6 +67485,7 @@
|
|||
"output_cost_per_token_priority": 9.375e-07,
|
||||
"source": "https://docs.fireworks.ai/serverless/pricing",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
|
|
@ -67457,6 +67503,7 @@
|
|||
"output_cost_per_token_priority": 6.25e-07,
|
||||
"source": "https://api.fireworks.ai/v1/serverless/models",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
|
|
@ -67475,6 +67522,7 @@
|
|||
"output_cost_per_token_priority": 9.375e-07,
|
||||
"source": "https://docs.fireworks.ai/serverless/pricing",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
|
|
@ -67493,6 +67541,7 @@
|
|||
"output_cost_per_token_priority": 4.05e-06,
|
||||
"source": "https://api.fireworks.ai/v1/serverless/models?format=nested",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
|
|
@ -68630,7 +68679,9 @@
|
|||
"cache_read_input_token_cost_above_272k_tokens": 5.4e-07,
|
||||
"output_cost_per_token": 1.62e-05,
|
||||
"output_cost_per_token_above_272k_tokens": 2.43e-05,
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-terra.html"
|
||||
"search_context_cost_per_query": {"search_context_size_high":0.0144,"search_context_size_low":0.0144,"search_context_size_medium":0.0144},
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-terra.html",
|
||||
"supports_web_search": true
|
||||
},
|
||||
"bedrock_mantle/us-gov-west-1/openai.gpt-5.6-luna": {
|
||||
"litellm_provider": "bedrock_mantle",
|
||||
|
|
@ -68665,7 +68716,9 @@
|
|||
"cache_read_input_token_cost_above_272k_tokens": 5.4e-08,
|
||||
"output_cost_per_token": 1.62e-06,
|
||||
"output_cost_per_token_above_272k_tokens": 2.43e-06,
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-luna.html"
|
||||
"search_context_cost_per_query": {"search_context_size_high":0.0144,"search_context_size_low":0.0144,"search_context_size_medium":0.0144},
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-luna.html",
|
||||
"supports_web_search": true
|
||||
},
|
||||
"bedrock_mantle/us-gov-west-1/openai.gpt-5.4": {
|
||||
"litellm_provider": "bedrock_mantle",
|
||||
|
|
@ -68694,7 +68747,9 @@
|
|||
"input_cost_per_token": 3.375e-06,
|
||||
"cache_read_input_token_cost": 3.375e-07,
|
||||
"output_cost_per_token": 2.025e-05,
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-54.html"
|
||||
"search_context_cost_per_query": {"search_context_size_high":0.0144,"search_context_size_low":0.0144,"search_context_size_medium":0.0144},
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-54.html",
|
||||
"supports_web_search": true
|
||||
},
|
||||
"bedrock_mantle/us-gov-west-1/xai.grok-4.3": {
|
||||
"use_openai_responses_path": true,
|
||||
|
|
@ -73728,6 +73783,7 @@
|
|||
"output_cost_per_token_priority": 1.875e-05,
|
||||
"source": "https://api.fireworks.ai/v1/serverless/models",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
@ -75309,6 +75365,7 @@
|
|||
"output_cost_per_token_above_272k_tokens": 1.65e-05,
|
||||
"output_cost_per_token_above_272k_tokens_ultrafast": 9.9e-05,
|
||||
"output_cost_per_token_ultrafast": 6.6e-05,
|
||||
"search_context_cost_per_query": {"search_context_size_high":0.012,"search_context_size_low":0.012,"search_context_size_medium":0.012},
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-6-1-sol.html",
|
||||
"supported_endpoints": [
|
||||
"/v1/chat/completions",
|
||||
|
|
@ -75331,6 +75388,7 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true,
|
||||
"supports_xhigh_reasoning_effort": true,
|
||||
"use_openai_responses_path": true
|
||||
},
|
||||
|
|
@ -77228,6 +77286,7 @@
|
|||
"output_cost_per_token_priority": 1.5e-06,
|
||||
"source": "https://api.fireworks.ai/v1/serverless/models?format=nested",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
|
|
@ -77386,6 +77445,7 @@
|
|||
"output_cost_per_token_priority": 9e-07,
|
||||
"source": "https://platform.minimax.io/docs/guides/pricing",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
|
|
@ -79019,6 +79079,7 @@
|
|||
"mode": "responses",
|
||||
"output_cost_per_token": 5.5e-06,
|
||||
"output_cost_per_token_above_272k_tokens": 8.25e-06,
|
||||
"search_context_cost_per_query": {"search_context_size_high":0.012,"search_context_size_low":0.012,"search_context_size_medium":0.012},
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-6-1-terra.html",
|
||||
"supported_endpoints": [
|
||||
"/v1/chat/completions",
|
||||
|
|
@ -79041,6 +79102,7 @@
|
|||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true,
|
||||
"supports_xhigh_reasoning_effort": true,
|
||||
"use_openai_responses_path": true
|
||||
},
|
||||
|
|
@ -81161,6 +81223,7 @@
|
|||
],
|
||||
"source": "https://api.fireworks.ai/v1/serverless/models",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
|
|
@ -81177,6 +81240,7 @@
|
|||
"output_cost_per_token": 3.08e-06,
|
||||
"source": "https://api.fireworks.ai/v1/serverless/models",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
|
|
@ -81193,6 +81257,7 @@
|
|||
"output_cost_per_token": 8.4e-07,
|
||||
"source": "https://api.fireworks.ai/v1/serverless/models",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
|
|
@ -81209,6 +81274,7 @@
|
|||
"output_cost_per_token": 8.4e-07,
|
||||
"source": "https://api.fireworks.ai/v1/serverless/models",
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
|
|
|
|||
|
|
@ -105,10 +105,10 @@ LAZY_FEATURES: Final[tuple[LazyFeature, ...]] = (
|
|||
LazyFeature(
|
||||
name="a2a",
|
||||
module_path="litellm.proxy.agent_endpoints.a2a_endpoints",
|
||||
# ``/v1/a2a/{agent_id}/message/send`` is caught via the suffix so the
|
||||
# ``/v1/a2a/{agent_id}/message/send`` and ``/message/stream`` are caught via suffixes so the
|
||||
# ``/v1/a2a`` prefix doesn't subsume the discover prefix below.
|
||||
path_prefixes=("/a2a",),
|
||||
path_suffixes=("/message/send",),
|
||||
path_suffixes=("/message/send", "/message/stream"),
|
||||
),
|
||||
LazyFeature(
|
||||
name="a2a_registration",
|
||||
|
|
|
|||
|
|
@ -54,6 +54,57 @@
|
|||
],
|
||||
"title": "ValidationError",
|
||||
"type": "object"
|
||||
},
|
||||
"_A2AJsonRpcResponse": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"error": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Error"
|
||||
},
|
||||
"id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Id"
|
||||
},
|
||||
"jsonrpc": {
|
||||
"const": "2.0",
|
||||
"title": "Jsonrpc",
|
||||
"type": "string"
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Result"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"jsonrpc",
|
||||
"id"
|
||||
],
|
||||
"title": "_A2AJsonRpcResponse",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -242,6 +293,119 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/a2a/{agent_id}/message/stream": {
|
||||
"post": {
|
||||
"description": "Invoke an agent using the A2A protocol (JSON-RPC 2.0).\n\nSupported methods:\n- message/send: Send a message and get a response\n- message/stream: Send a message and stream the response",
|
||||
"operationId": "invoke_agent_a2a_a2a__agent_id__message_stream_post",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "agent_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"title": "Agent Id",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"id": "stream-1",
|
||||
"jsonrpc": "2.0",
|
||||
"method": "message/stream",
|
||||
"params": {
|
||||
"message": {
|
||||
"kind": "message",
|
||||
"messageId": "message-1",
|
||||
"parts": [
|
||||
{
|
||||
"kind": "text",
|
||||
"text": "Hello"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Id"
|
||||
},
|
||||
"jsonrpc": {
|
||||
"const": "2.0",
|
||||
"title": "Jsonrpc",
|
||||
"type": "string"
|
||||
},
|
||||
"method": {
|
||||
"title": "Method",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"additionalProperties": true,
|
||||
"title": "Params",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"jsonrpc",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "_A2AJsonRpcRequest",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/_A2AJsonRpcResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Successful Response"
|
||||
},
|
||||
"422": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Validation Error"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"APIKeyHeader": []
|
||||
}
|
||||
],
|
||||
"summary": "Invoke Agent A2A",
|
||||
"tags": [
|
||||
"a2a"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/a2a/{agent_id}/message/send": {
|
||||
"post": {
|
||||
"description": "Invoke an agent using the A2A protocol (JSON-RPC 2.0).\n\nSupported methods:\n- message/send: Send a message and get a response\n- message/stream: Send a message and stream the response",
|
||||
|
|
@ -287,6 +451,119 @@
|
|||
"a2a"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/a2a/{agent_id}/message/stream": {
|
||||
"post": {
|
||||
"description": "Invoke an agent using the A2A protocol (JSON-RPC 2.0).\n\nSupported methods:\n- message/send: Send a message and get a response\n- message/stream: Send a message and stream the response",
|
||||
"operationId": "invoke_agent_a2a_v1_a2a__agent_id__message_stream_post",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "agent_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"title": "Agent Id",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"id": "stream-1",
|
||||
"jsonrpc": "2.0",
|
||||
"method": "message/stream",
|
||||
"params": {
|
||||
"message": {
|
||||
"kind": "message",
|
||||
"messageId": "message-1",
|
||||
"parts": [
|
||||
{
|
||||
"kind": "text",
|
||||
"text": "Hello"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Id"
|
||||
},
|
||||
"jsonrpc": {
|
||||
"const": "2.0",
|
||||
"title": "Jsonrpc",
|
||||
"type": "string"
|
||||
},
|
||||
"method": {
|
||||
"title": "Method",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"additionalProperties": true,
|
||||
"title": "Params",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"jsonrpc",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "_A2AJsonRpcRequest",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/_A2AJsonRpcResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Successful Response"
|
||||
},
|
||||
"422": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Validation Error"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"APIKeyHeader": []
|
||||
}
|
||||
],
|
||||
"summary": "Invoke Agent A2A",
|
||||
"tags": [
|
||||
"a2a"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -15326,6 +15603,10 @@
|
|||
],
|
||||
"title": "Metadata"
|
||||
},
|
||||
"response_format": {
|
||||
"$ref": "#/components/schemas/GuardrailResponseFormat",
|
||||
"default": "standard"
|
||||
},
|
||||
"text": {
|
||||
"title": "Text",
|
||||
"type": "string"
|
||||
|
|
@ -16203,6 +16484,14 @@
|
|||
"title": "BedrockChecksSensitiveInformationModel",
|
||||
"type": "object"
|
||||
},
|
||||
"BedrockGuardrailOutputScope": {
|
||||
"enum": [
|
||||
"INTERVENTIONS",
|
||||
"FULL"
|
||||
],
|
||||
"title": "BedrockGuardrailOutputScope",
|
||||
"type": "string"
|
||||
},
|
||||
"BlockedWord": {
|
||||
"description": "Represents a blocked word with its action and optional description",
|
||||
"properties": {
|
||||
|
|
@ -16746,6 +17035,14 @@
|
|||
"title": "GuardrailInfoResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"GuardrailResponseFormat": {
|
||||
"enum": [
|
||||
"standard",
|
||||
"native"
|
||||
],
|
||||
"title": "GuardrailResponseFormat",
|
||||
"type": "string"
|
||||
},
|
||||
"GuardrailSubmissionItem": {
|
||||
"properties": {
|
||||
"created_at": {
|
||||
|
|
@ -16998,6 +17295,7 @@
|
|||
"title": "HTTPValidationError",
|
||||
"type": "object"
|
||||
},
|
||||
"JsonValue": {},
|
||||
"LakeraCategoryThresholds": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
|
|
@ -17924,6 +18222,18 @@
|
|||
"description": "Whether to fail the request if the guardrail encounters an error. Implemented by guardrail='model_armor', 'generic_guardrail_api' and 'crowdstrike_aidr'. True (default) raises the error. False logs a critical error and lets the request proceed, so only a valid guardrail response can block or modify it.",
|
||||
"title": "Fail On Error"
|
||||
},
|
||||
"file_checkpoint_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "File-checkpoint ID for the Ovalix Tracker service (falls back to pre/post).",
|
||||
"title": "File Checkpoint Id"
|
||||
},
|
||||
"file_guardrail_timeout": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
|
@ -18426,6 +18736,17 @@
|
|||
],
|
||||
"description": "Optional parameters for the guardrail"
|
||||
},
|
||||
"outputScope": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/BedrockGuardrailOutputScope"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "ApplyGuardrail response scope. FULL includes detected and non-detected findings. Omit to retain Bedrock's default of returning detected findings only."
|
||||
},
|
||||
"output_parse_pii": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
|
@ -20308,7 +20629,24 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApplyGuardrailResponse"
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ApplyGuardrailResponse"
|
||||
},
|
||||
{
|
||||
"additionalProperties": {
|
||||
"$ref": "#/components/schemas/JsonValue"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/JsonValue"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"title": "Response Apply Guardrail Apply Guardrail Post"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -20400,7 +20738,24 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApplyGuardrailResponse"
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ApplyGuardrailResponse"
|
||||
},
|
||||
{
|
||||
"additionalProperties": {
|
||||
"$ref": "#/components/schemas/JsonValue"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/JsonValue"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"title": "Response Apply Guardrail Guardrails Apply Guardrail Post"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -501,6 +501,7 @@ class LiteLLMRoutes(enum.Enum):
|
|||
"/a2a/{agent_id}/message/send",
|
||||
"/v1/a2a/{agent_id}/message/send",
|
||||
"/a2a/{agent_id}/message/stream",
|
||||
"/v1/a2a/{agent_id}/message/stream",
|
||||
"/a2a/{agent_id}/.well-known/agent-card.json",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ from collections.abc import Mapping
|
|||
from copy import deepcopy
|
||||
from typing import Final, Literal
|
||||
|
||||
from litellm.litellm_core_utils.core_helpers import as_str_mapping, iter_items
|
||||
|
||||
SupportedA2AVersion = Literal["0.3", "1.0"]
|
||||
|
||||
# Protocol versions LiteLLM can serve to A2A clients. The admin pins one per agent;
|
||||
|
|
@ -21,7 +23,6 @@ SUPPORTED_A2A_PROTOCOL_VERSIONS: Final[tuple[SupportedA2AVersion, ...]] = ("0.3"
|
|||
# Default served version when the agent card does not pin one.
|
||||
LITELLM_A2A_PROTOCOL_VERSION: Final = "1.0"
|
||||
|
||||
|
||||
_PROTOCOL_VERSION_PATTERN: Final = re.compile(
|
||||
r"^(\d+\.\d+)(?:\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)?$"
|
||||
)
|
||||
|
|
@ -50,6 +51,24 @@ def resolve_served_protocol_version(card: Mapping[str, object] | None) -> str:
|
|||
return normalized if normalized is not None else LITELLM_A2A_PROTOCOL_VERSION
|
||||
|
||||
|
||||
def point_agent_card_at_proxy(
|
||||
agent_card: Mapping[str, object],
|
||||
proxy_url: str,
|
||||
) -> dict[str, object]: # mutable-ok: the public helper contract requires a dictionary
|
||||
interfaces: Final = agent_card.get("supportedInterfaces")
|
||||
interface_items: Final = (
|
||||
tuple(iter_items(agent_card.get("supportedInterfaces"))) if isinstance(interfaces, list) else ()
|
||||
)
|
||||
first: Final[object] = interface_items[0] if interface_items else None
|
||||
first_mapping: Final = as_str_mapping(first)
|
||||
pointed_interfaces: Final = (
|
||||
{"supportedInterfaces": [{**first_mapping, "url": proxy_url}, *interface_items[1:]]}
|
||||
if isinstance(first, dict) and first_mapping is not None
|
||||
else {}
|
||||
)
|
||||
return deepcopy({**agent_card, "url": proxy_url, **pointed_interfaces})
|
||||
|
||||
|
||||
# Security scheme exposed by the LiteLLM-fronted agent card. Always replaces
|
||||
# whatever upstream advertised — the client must authenticate to the proxy,
|
||||
# not the upstream agent.
|
||||
|
|
|
|||
|
|
@ -12,9 +12,8 @@ The A2A SDK can point to LiteLLM's URL and invoke agents registered with LiteLLM
|
|||
|
||||
import json
|
||||
from collections.abc import AsyncGenerator, Callable, Coroutine, Mapping
|
||||
from copy import deepcopy
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Final, Protocol
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, Protocol
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||
|
|
@ -29,6 +28,7 @@ from litellm.litellm_core_utils.url_utils import SSRFError, validate_url
|
|||
from litellm.llms.a2a.common_utils import resolve_a2a_hop_auth_header
|
||||
from litellm.llms.custom_httpx.sse_lines import aiter_sse_lines
|
||||
from litellm.proxy._types import ProxyException, UserAPIKeyAuth
|
||||
from litellm.proxy.a2a.agent_card import point_agent_card_at_proxy
|
||||
from litellm.proxy.a2a.version_convert import (
|
||||
A2AVersion,
|
||||
normalize_agent_card,
|
||||
|
|
@ -43,6 +43,7 @@ from litellm.proxy.agent_endpoints.databricks_oauth import (
|
|||
)
|
||||
from litellm.proxy.agent_endpoints.utils import merge_agent_headers
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.common_utils.custom_openapi_spec import inline_request_body
|
||||
from litellm.proxy.common_utils.openai_error_payload import status_typed_proxy_exception
|
||||
from litellm.proxy.common_utils.sse_keepalive import (
|
||||
SSE_COMMENT_PING,
|
||||
|
|
@ -59,6 +60,42 @@ if TYPE_CHECKING:
|
|||
|
||||
router: Final = APIRouter()
|
||||
|
||||
|
||||
class _A2AJsonRpcRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="allow", frozen=True)
|
||||
|
||||
jsonrpc: Literal["2.0"]
|
||||
id: str | int | None = None
|
||||
method: str
|
||||
params: Mapping[str, object]
|
||||
|
||||
|
||||
class _A2AJsonRpcResponse(BaseModel):
|
||||
model_config = ConfigDict(extra="allow", frozen=True)
|
||||
|
||||
jsonrpc: Literal["2.0"]
|
||||
id: str | int | None
|
||||
result: object | None = None
|
||||
error: Mapping[str, object] | None = None
|
||||
|
||||
|
||||
_A2A_MESSAGE_STREAM_REQUEST_BODY: Final = inline_request_body(
|
||||
_A2AJsonRpcRequest,
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "stream-1",
|
||||
"method": "message/stream",
|
||||
"params": {
|
||||
"message": {
|
||||
"messageId": "message-1",
|
||||
"role": "user",
|
||||
"parts": [{"kind": "text", "text": "Hello"}],
|
||||
"kind": "message",
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
# Mirrors the native seam's own headers: a reverse proxy that batches the whole
|
||||
# stream would swallow the keepalives this route sends to defeat idle timeouts.
|
||||
_SSE_KEEPALIVE_HEADERS: Final[Mapping[str, str]] = MappingProxyType(
|
||||
|
|
@ -206,6 +243,35 @@ def _forwarding_headers(
|
|||
return merged or None
|
||||
|
||||
|
||||
async def _log_agent_rejection(
|
||||
*,
|
||||
proxy_logging_obj: ProxyLogging,
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
request_id: object,
|
||||
method: str | None,
|
||||
rejection: HTTPException,
|
||||
target_agent_id: str,
|
||||
model: str,
|
||||
) -> None:
|
||||
request_data: Final[dict[str, object]] = { # mutable-ok: the failure hook stamps its logging state onto it
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"method": method,
|
||||
"model": model,
|
||||
"agent_id": target_agent_id,
|
||||
"metadata": {"agent_id": target_agent_id},
|
||||
}
|
||||
try:
|
||||
await proxy_logging_obj.post_call_failure_hook( # pyright: ignore[reportUnknownMemberType] # bare dict in sig
|
||||
request_data=request_data,
|
||||
original_exception=rejection,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
route="/a2a/{agent_id}",
|
||||
)
|
||||
except Exception: # noqa: BLE001 # a logging failure must not change the rejection the caller gets
|
||||
verbose_proxy_logger.exception("Failed to log rejected A2A request")
|
||||
|
||||
|
||||
def _jsonrpc_error(
|
||||
request_id: object,
|
||||
code: int,
|
||||
|
|
@ -684,16 +750,12 @@ async def get_agent_card(
|
|||
)
|
||||
|
||||
proxy_url: Final = get_custom_url(str(request.base_url), route=f"a2a/{agent_id}")
|
||||
agent_card = deepcopy(agent.agent_card_params)
|
||||
agent_card["url"] = proxy_url
|
||||
interfaces: Final = agent_card.get("supportedInterfaces")
|
||||
if isinstance(interfaces, list) and interfaces:
|
||||
interfaces[0]["url"] = proxy_url
|
||||
agent_card: Final = point_agent_card_at_proxy(agent.agent_card_params, proxy_url)
|
||||
served_version: Final = _served_version(agent, request)
|
||||
agent_card = normalize_agent_card(agent_card, served_version)
|
||||
normalized_agent_card: Final = normalize_agent_card(agent_card, served_version)
|
||||
|
||||
verbose_proxy_logger.debug("Returning agent card for '%s' with proxy URL: %s", agent_id, proxy_url)
|
||||
return JSONResponse(content=agent_card)
|
||||
return JSONResponse(content=normalized_agent_card)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
|
|
@ -717,6 +779,20 @@ async def get_agent_card(
|
|||
tags=["[beta] A2A Agents"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
@router.post(
|
||||
"/a2a/{agent_id}/message/stream",
|
||||
tags=["[beta] A2A Agents"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
openapi_extra={"requestBody": _A2A_MESSAGE_STREAM_REQUEST_BODY},
|
||||
responses={200: {"model": _A2AJsonRpcResponse}},
|
||||
)
|
||||
@router.post(
|
||||
"/v1/a2a/{agent_id}/message/stream",
|
||||
tags=["[beta] A2A Agents"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
openapi_extra={"requestBody": _A2A_MESSAGE_STREAM_REQUEST_BODY},
|
||||
responses={200: {"model": _A2AJsonRpcResponse}},
|
||||
)
|
||||
async def invoke_agent_a2a(
|
||||
agent_id: str,
|
||||
request: Request,
|
||||
|
|
@ -778,6 +854,15 @@ async def invoke_agent_a2a(
|
|||
# Find the agent
|
||||
agent: Final = await _get_agent(agent_id)
|
||||
if agent is None:
|
||||
await _log_agent_rejection(
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
request_id=request_id,
|
||||
method=method,
|
||||
rejection=HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found"),
|
||||
target_agent_id=agent_id,
|
||||
model=f"a2a_agent/{agent_id}",
|
||||
)
|
||||
return _jsonrpc_error(request_id, -32000, f"Agent '{agent_id}' not found", 404)
|
||||
|
||||
served_version: Final = _served_version(agent, request, original_method)
|
||||
|
|
@ -787,10 +872,20 @@ async def invoke_agent_a2a(
|
|||
user_api_key_auth=user_api_key_dict,
|
||||
)
|
||||
if not is_allowed:
|
||||
raise HTTPException(
|
||||
denial: Final = HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Agent '{agent_id}' is not allowed for your key/team. Contact proxy admin for access.",
|
||||
)
|
||||
await _log_agent_rejection(
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
request_id=request_id,
|
||||
method=method,
|
||||
rejection=denial,
|
||||
target_agent_id=agent.agent_id,
|
||||
model=f"a2a_agent/{(as_str_mapping(agent.agent_card_params or {}) or {}).get('name', agent_id)}",
|
||||
)
|
||||
raise denial
|
||||
|
||||
user_api_key_dict.invoked_agent_id = agent.agent_id
|
||||
strict_policy: Final = agent.identity_managed or bool(user_api_key_dict.requires_fresh_policy)
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ async def search_agents(
|
|||
user_api_key_dict: UserAPIKeyAuth,
|
||||
proxy_logging_obj: ProxyLogging,
|
||||
) -> AgentSearchOutcome:
|
||||
if embedding_model is None:
|
||||
if embedding_model is None or not embedding_model.strip():
|
||||
return AgentSearchNotConfigured(
|
||||
reason="agent search needs litellm_settings.agent_search_embedding_model set to an embedding model from model_list"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ from typing_extensions import ReadOnly, Required, assert_never
|
|||
import litellm
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.constants import AGENT_HEALTH_CHECK_GATHER_TIMEOUT_SECONDS, AGENT_HEALTH_CHECK_TIMEOUT_SECONDS
|
||||
from litellm.litellm_core_utils.core_helpers import as_str_mapping, iter_items
|
||||
from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
|
||||
from litellm.proxy._types import (
|
||||
CommonProxyErrors,
|
||||
|
|
@ -33,6 +34,7 @@ from litellm.proxy.a2a.agent_card import (
|
|||
SUPPORTED_A2A_PROTOCOL_VERSIONS,
|
||||
merge_agent_card,
|
||||
normalize_protocol_version,
|
||||
point_agent_card_at_proxy,
|
||||
)
|
||||
from litellm.proxy.agent_endpoints.agent_registry import (
|
||||
AgentIdWhere,
|
||||
|
|
@ -180,10 +182,36 @@ def _redact_agent_litellm_params_dict(
|
|||
return dict(parse_agent_litellm_params(redact_sensitive_agent_litellm_params(litellm_params)))
|
||||
|
||||
|
||||
def _non_admin_agent_card(
|
||||
card: Mapping[str, object], proxy_url: str
|
||||
) -> dict[str, object]: # mutable-ok: AgentResponse.agent_card_params requires a mutable dictionary
|
||||
pointed_card: Final = point_agent_card_at_proxy(card, proxy_url)
|
||||
interfaces: Final = pointed_card.get("supportedInterfaces")
|
||||
interface_items: Final = (
|
||||
tuple(iter_items(pointed_card.get("supportedInterfaces"))) if isinstance(interfaces, list) else ()
|
||||
)
|
||||
first_interface: Final[object] = interface_items[0] if interface_items else None
|
||||
first_mapping: Final = as_str_mapping(first_interface)
|
||||
proxy_interface: Final = (
|
||||
{"supportedInterfaces": [{**first_mapping, "url": proxy_url}]}
|
||||
if isinstance(first_interface, dict) and first_mapping is not None and first_mapping.get("url") == proxy_url
|
||||
else {}
|
||||
)
|
||||
return {
|
||||
**{
|
||||
key: value
|
||||
for key, value in pointed_card.items()
|
||||
if key not in ("additionalInterfaces", "supportedInterfaces")
|
||||
},
|
||||
**proxy_interface,
|
||||
}
|
||||
|
||||
|
||||
def _redact_sensitive_agent_fields(
|
||||
agents: Sequence[AgentResponse],
|
||||
*,
|
||||
is_admin: bool,
|
||||
request_base_url: str,
|
||||
) -> list[AgentResponse]:
|
||||
"""
|
||||
Return copies of the given agents with credential-bearing litellm_params
|
||||
|
|
@ -207,6 +235,11 @@ def _redact_sensitive_agent_fields(
|
|||
copy.keys = None
|
||||
copy.kill_switch = None
|
||||
copy.identity = None
|
||||
if copy.agent_card_params:
|
||||
copy.agent_card_params = _non_admin_agent_card(
|
||||
copy.agent_card_params,
|
||||
get_custom_url(request_base_url, route=f"a2a/{copy.agent_id}"),
|
||||
)
|
||||
if copy.litellm_params:
|
||||
copy.litellm_params = _redact_agent_litellm_params_dict(copy.litellm_params)
|
||||
copy.kill_switch = redact_kill_switch(copy.kill_switch)
|
||||
|
|
@ -461,14 +494,6 @@ async def get_agents(
|
|||
global_agent_registry.ids_for_agent(agent.agent_id).isdisjoint(litellm.public_agent_groups)
|
||||
)
|
||||
|
||||
# litellm_params secrets are always redacted; keys/headers stay
|
||||
# admin-only.
|
||||
is_admin: Final = (
|
||||
user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN
|
||||
or user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value
|
||||
)
|
||||
returned_agents = _redact_sensitive_agent_fields(returned_agents, is_admin=is_admin)
|
||||
|
||||
if health_check:
|
||||
agents_with_url: Final = [agent for agent in returned_agents if (agent.agent_card_params or {}).get("url")]
|
||||
agents_without_url = [agent for agent in returned_agents if not (agent.agent_card_params or {}).get("url")]
|
||||
|
|
@ -493,6 +518,18 @@ async def get_agents(
|
|||
healthy_ids: Final = {result["agent_id"] for result in health_results if result["healthy"]}
|
||||
returned_agents = [agent for agent in agents_with_url if agent.agent_id in healthy_ids] + agents_without_url
|
||||
|
||||
# litellm_params secrets are always redacted; keys/headers stay
|
||||
# admin-only.
|
||||
is_admin: Final = (
|
||||
user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN
|
||||
or user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value
|
||||
)
|
||||
returned_agents = _redact_sensitive_agent_fields(
|
||||
returned_agents,
|
||||
is_admin=is_admin,
|
||||
request_base_url=str(request.base_url),
|
||||
)
|
||||
|
||||
if query is None:
|
||||
return returned_agents
|
||||
return await _rank_agents_by_query(query, returned_agents, top_k, user_api_key_dict)
|
||||
|
|
@ -691,7 +728,11 @@ async def create_agent(
|
|||
|
||||
# The caller is a proxy admin (enforced above); litellm_params
|
||||
# secrets are still never echoed back in the response.
|
||||
return _redact_sensitive_agent_fields((result,), is_admin=True)[0]
|
||||
return _redact_sensitive_agent_fields(
|
||||
(result,),
|
||||
is_admin=True,
|
||||
request_base_url=str(http_request.base_url),
|
||||
)[0]
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
|
|
@ -708,6 +749,7 @@ async def create_agent(
|
|||
)
|
||||
async def get_agent_by_id(
|
||||
agent_id: str,
|
||||
request: Request,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
|
|
@ -770,7 +812,11 @@ async def get_agent_by_id(
|
|||
user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN
|
||||
or user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value
|
||||
)
|
||||
agent = _redact_sensitive_agent_fields((agent,), is_admin=is_admin)[0]
|
||||
agent = _redact_sensitive_agent_fields(
|
||||
(agent,),
|
||||
is_admin=is_admin,
|
||||
request_base_url=str(request.base_url),
|
||||
)[0]
|
||||
|
||||
return agent
|
||||
except HTTPException:
|
||||
|
|
@ -879,7 +925,11 @@ async def update_agent(
|
|||
"Successfully updated agent '%s' (ID: %s) in memory", existing_agent.get("agent_name"), agent_id
|
||||
)
|
||||
|
||||
return _redact_sensitive_agent_fields((result,), is_admin=True)[0]
|
||||
return _redact_sensitive_agent_fields(
|
||||
(result,),
|
||||
is_admin=True,
|
||||
request_base_url=str(http_request.base_url),
|
||||
)[0]
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
|
@ -987,7 +1037,11 @@ async def patch_agent(
|
|||
"Successfully updated agent '%s' (ID: %s) in memory", existing_agent.get("agent_name"), agent_id
|
||||
)
|
||||
|
||||
return _redact_sensitive_agent_fields((result,), is_admin=True)[0]
|
||||
return _redact_sensitive_agent_fields(
|
||||
(result,),
|
||||
is_admin=True,
|
||||
request_base_url=str(http_request.base_url),
|
||||
)[0]
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -5378,79 +5378,6 @@ async def _granted_model_lists(
|
|||
)
|
||||
|
||||
|
||||
async def _user_object_or_none(
|
||||
valid_token: UserAPIKeyAuth,
|
||||
prisma_client: PrismaClient,
|
||||
user_api_key_cache: UserApiKeyCache,
|
||||
proxy_logging_obj: ProxyLogging,
|
||||
) -> LiteLLM_UserTable | None:
|
||||
try:
|
||||
return await get_user_object(
|
||||
user_id=valid_token.user_id,
|
||||
prisma_client=prisma_client,
|
||||
user_api_key_cache=user_api_key_cache,
|
||||
user_id_upsert=False,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
)
|
||||
except UserNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
async def enforced_model_allowlists(
|
||||
valid_token: UserAPIKeyAuth,
|
||||
prisma_client: PrismaClient | None,
|
||||
user_api_key_cache: UserApiKeyCache,
|
||||
proxy_logging_obj: ProxyLogging,
|
||||
) -> tuple[Sequence[str], ...]:
|
||||
"""One model allowlist per level that ``common_checks`` enforces on a request from this identity."""
|
||||
key_models: Final = _resolve_key_models_for_auth_check(valid_token=valid_token)
|
||||
if prisma_client is None:
|
||||
return (key_models, tuple(valid_token.team_models or ()))
|
||||
team_object: Final = (
|
||||
None
|
||||
if valid_token.team_id is None
|
||||
else await get_team_object(
|
||||
team_id=valid_token.team_id,
|
||||
prisma_client=prisma_client,
|
||||
user_api_key_cache=user_api_key_cache,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
)
|
||||
)
|
||||
user_object: Final = (
|
||||
None
|
||||
if team_object is not None
|
||||
else await _user_object_or_none(
|
||||
valid_token=valid_token,
|
||||
prisma_client=prisma_client,
|
||||
user_api_key_cache=user_api_key_cache,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
)
|
||||
)
|
||||
project_object: Final = (
|
||||
None
|
||||
if valid_token.project_id is None
|
||||
else await get_project_object(
|
||||
project_id=valid_token.project_id,
|
||||
prisma_client=prisma_client,
|
||||
user_api_key_cache=user_api_key_cache,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
)
|
||||
)
|
||||
return (
|
||||
key_models,
|
||||
team_object.models if team_object is not None else (),
|
||||
await _team_member_granted_models(
|
||||
valid_token=valid_token,
|
||||
team_object=team_object,
|
||||
prisma_client=prisma_client,
|
||||
user_api_key_cache=user_api_key_cache,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
),
|
||||
user_object.models if user_object is not None else (),
|
||||
project_object.models if project_object is not None else (),
|
||||
)
|
||||
|
||||
|
||||
async def collect_matched_model_access_groups(
|
||||
model: str | Sequence[str] | None,
|
||||
valid_token: UserAPIKeyAuth | None,
|
||||
|
|
|
|||
|
|
@ -770,9 +770,10 @@ def codex_model_catalog(
|
|||
A model Codex does not know gets the fallback
|
||||
entry, with the same base instructions Codex itself uses so the agent never
|
||||
runs without a system prompt, and offers the reasoning efforts `groups` gives its model group
|
||||
that the installed Codex, at `client_version`, can parse. Apart from aliases, the catalog is
|
||||
the one `/v1/models?client_version=...` serves, built from the installed Codex's own entries
|
||||
instead of the proxy's vendored ones.
|
||||
that the installed Codex, at `client_version`, can parse. Apart from aliases, and the window the
|
||||
deployments' `model_info.max_input_tokens` gives a model Codex knows, the catalog is the one
|
||||
`/v1/models?client_version=...` serves, built from the installed Codex's own entries instead of
|
||||
the proxy's vendored ones.
|
||||
"""
|
||||
rows: Final = tuple(_codex_row(m, upstreams.get(m.id), groups) for m in models)
|
||||
body: Final = codex_models_response_json(
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ never the OpenAI list shape. This module builds that body from the rows ``/v1/mo
|
|||
lists: a model Codex knows keeps Codex's own entry for it, any other model gets the fallback entry
|
||||
Codex itself uses for an unknown slug, and ``model_info.service_tiers`` becomes the entry's
|
||||
``service_tiers``, which Codex turns into slash commands (``/ultrafast``) that send ``service_tier``
|
||||
upstream. A fallback entry offers the model group's ``supported_reasoning_efforts`` as its reasoning
|
||||
levels, trimmed to the ones the requesting Codex release can parse.
|
||||
upstream. A ``model_info.max_input_tokens`` the operator sets replaces a known model's
|
||||
``context_window``, up to the most Codex lets its own ``model_context_window`` raise that model to.
|
||||
A fallback entry offers the model group's ``supported_reasoning_efforts`` as its reasoning levels,
|
||||
trimmed to the ones the requesting Codex release can parse.
|
||||
|
||||
The stock entries are Codex 0.159.3's bundled catalog, vendored unchanged from
|
||||
https://github.com/openai/codex/blob/rust-v0.159.3/codex-rs/models-manager/models.json (Apache-2.0)
|
||||
|
|
@ -158,6 +160,7 @@ class CodexStockModel(LiteLLMBaseModel):
|
|||
supports_search_tool: bool = False
|
||||
supported_reasoning_levels: tuple[CodexReasoningLevel, ...] = ()
|
||||
context_window: int | None = None
|
||||
max_context_window: int | None = None
|
||||
|
||||
|
||||
class CodexStockCatalog(LiteLLMBaseModel):
|
||||
|
|
@ -173,8 +176,9 @@ class CodexCatalogRow:
|
|||
"""What the catalog needs to know about one listed model: its public id, the listing's mode and
|
||||
input limit, the upstream model behind it, the reasoning effort its deployments pin, and the
|
||||
`model_info` values the operator set; `service_tiers` holds one raw `model_info.service_tiers`
|
||||
value per deployment behind the id, None where a deployment sets none. `supported_reasoning_efforts`
|
||||
is the model group's, None when unknown."""
|
||||
value per deployment behind the id, None where a deployment sets none, and
|
||||
`configured_max_input_tokens` one `model_info.max_input_tokens` per deployment the same way.
|
||||
`supported_reasoning_efforts` is the model group's, None when unknown."""
|
||||
|
||||
id: str
|
||||
mode: str | None = None
|
||||
|
|
@ -185,6 +189,7 @@ class CodexCatalogRow:
|
|||
service_tiers: tuple[object, ...] = ()
|
||||
supported_reasoning_efforts: tuple[str, ...] | None = None
|
||||
search_tool_support: tuple[bool | None, ...] = ()
|
||||
configured_max_input_tokens: tuple[int | None, ...] = ()
|
||||
|
||||
|
||||
@cache
|
||||
|
|
@ -333,6 +338,25 @@ def _alias_context_window(row: CodexCatalogRow, stock_model: CodexStockModel) ->
|
|||
return MappingProxyType({"context_window": min(stock_model.context_window, row.max_input_tokens)})
|
||||
|
||||
|
||||
def _configured_window(row: CodexCatalogRow, stock_model: CodexStockModel) -> Mapping[str, int]:
|
||||
"""The `context_window` the operator's `model_info.max_input_tokens` gives a known model's entry,
|
||||
or nothing, which keeps `stock_model`'s own.
|
||||
|
||||
A request to the row can route to any of its deployments, so the window is the smallest one of them
|
||||
takes: its `max_input_tokens` where the operator set a positive one, Codex's own window where not. A
|
||||
limit set on only some deployments can therefore lower the window but never raise it. The window never
|
||||
goes past `stock_model`'s `max_context_window`, the most Codex lets its own `model_context_window`
|
||||
setting raise that model to."""
|
||||
per_deployment: Final = row.configured_max_input_tokens
|
||||
declared: Final = tuple(limit for limit in per_deployment if limit is not None and limit > 0)
|
||||
unset_window: Final = () if len(declared) == len(per_deployment) else (stock_model.context_window,)
|
||||
if not declared or None in unset_window:
|
||||
return MappingProxyType({})
|
||||
smallest: Final = min(limit for limit in (*declared, *unset_window) if limit is not None)
|
||||
ceiling: Final = stock_model.max_context_window
|
||||
return MappingProxyType({"context_window": smallest if ceiling is None else min(smallest, ceiling)})
|
||||
|
||||
|
||||
def _alias_reasoning_level(row: CodexCatalogRow, stock_model: CodexStockModel) -> str | None:
|
||||
efforts: Final = frozenset(level.effort for level in stock_model.supported_reasoning_levels)
|
||||
return row.reasoning_effort if row.reasoning_effort in efforts else None
|
||||
|
|
@ -398,6 +422,7 @@ def _entry(
|
|||
"upgrade": upgrade,
|
||||
**({} if search_tool is None else {"supports_search_tool": search_tool}),
|
||||
**(_deployment_defaults(row, stock_model) if is_alias and alias_defaults == "deployment" else {}),
|
||||
**_configured_window(row, stock_model),
|
||||
}
|
||||
if configured is None:
|
||||
return stock_model.model_copy(update=listing)
|
||||
|
|
@ -473,10 +498,12 @@ def codex_models_response_json(
|
|||
byte limit silently and keeps its bundled one, so the body holds only the entries that fit,
|
||||
the ones offering a service tier taken first and the rest in listing order, an entry too large
|
||||
for the bytes left passed over, every kept entry at its listing position; `byte_limit=None`
|
||||
keeps every entry. A row listed under another name than the Codex model its `upstream_model`
|
||||
names (an alias) takes that model's entry; `alias_defaults="deployment"` drops the defaults
|
||||
Codex picks for that model in favor of what the alias's deployments pin, while `"codex"` keeps
|
||||
them, as Codex would use them for the model's own name. `client_version` is the requesting Codex's version, which decides the reasoning
|
||||
keeps every entry. A row whose deployments set `model_info.max_input_tokens` gets the smallest
|
||||
window any of them takes as its known model's `context_window`, capped at that model's
|
||||
`max_context_window`. A row listed under another name than the Codex model its `upstream_model` names (an alias) takes that model's
|
||||
entry; `alias_defaults="deployment"` drops the defaults Codex picks for that model in favor of
|
||||
what the alias's deployments pin, while `"codex"` keeps them, as Codex would use them for the
|
||||
model's own name. `client_version` is the requesting Codex's version, which decides the reasoning
|
||||
levels a fallback entry may offer; None counts as a version too old for custom levels.
|
||||
"""
|
||||
known: Final = bundled_codex_models() if stock is None else stock
|
||||
|
|
@ -519,6 +546,9 @@ def _catalog_row(
|
|||
search_tool_support=(
|
||||
registry.resolve(lookup_id, kind="search_tool_support", team_id=team_id) if registry is not None else ()
|
||||
),
|
||||
configured_max_input_tokens=(
|
||||
registry.group(lookup_id, field="max_input_tokens", team_id=team_id) if registry is not None else ()
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -532,9 +562,9 @@ def codex_catalog_rows(
|
|||
"""`rows` joined with the router's configured metadata, looked up by each entry's internal id so
|
||||
team-scoped rows resolve the way the Anthropic listing's display names do, a `model_group_alias`
|
||||
reading its target's deployments under its own name; `team_id` is the requesting key's team, so the
|
||||
upstream model that picks Codex's stock entry and the tiers are read only off the deployments its
|
||||
requests can route to, and the reasoning efforts are the ones `/model_group/info` reports for the
|
||||
entry. Display names are read under `label_entries`' ids when given."""
|
||||
upstream model that picks Codex's stock entry, the tiers, and the configured input limits are read
|
||||
only off the deployments its requests can route to, and the reasoning efforts are the ones
|
||||
`/model_group/info` reports for the entry. Display names are read under `label_entries`' ids when given."""
|
||||
lookup_ids: Final = MappingProxyType(dict(entries))
|
||||
label_ids: Final = lookup_ids if label_entries is None else MappingProxyType(dict(label_entries))
|
||||
return tuple(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
from functools import lru_cache
|
||||
from typing import Final
|
||||
|
||||
|
|
@ -10,7 +11,7 @@ def realtime_request_body(model: str | None) -> bytes:
|
|||
Generate the realtime websocket request body. Cached with LRU semantics to avoid repeated
|
||||
string formatting work while keeping memory usage bounded.
|
||||
"""
|
||||
return f'{{"model": "{model or ""}"}}'.encode()
|
||||
return json.dumps({"model": model or ""}).encode()
|
||||
|
||||
|
||||
_realtime_request_body: Final = realtime_request_body
|
||||
|
|
|
|||
|
|
@ -56,6 +56,22 @@ class TokenCountSettings:
|
|||
framing: int | None
|
||||
count_model: TokenCountModel | None
|
||||
tokenizer: TokenCountTokenizer | None = None
|
||||
tool_framing: int | None = None
|
||||
responses_framing: int | None = None
|
||||
responses_tool_framing: int | None = None
|
||||
|
||||
@property
|
||||
def frames_counts(self) -> bool:
|
||||
return any(
|
||||
framing is not None
|
||||
for framing in (self.framing, self.tool_framing, self.responses_framing, self.responses_tool_framing)
|
||||
)
|
||||
|
||||
def hidden_prompt_tokens(self, carries_tools: bool, responses_api: bool) -> int:
|
||||
chat: Final = self.tool_framing if carries_tools else None
|
||||
responses: Final = self.responses_tool_framing if carries_tools else self.responses_framing
|
||||
narrower: Final = responses if responses_api else chat
|
||||
return (narrower if narrower is not None else self.framing) or 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
|
@ -105,6 +121,15 @@ def _framing(value: object) -> int | None:
|
|||
return None
|
||||
|
||||
|
||||
def _first_framing(source: Mapping[str, object], keys: tuple[str, ...]) -> int | None:
|
||||
return next((framing for framing in map(_framing, map(source.get, keys)) if framing is not None), None)
|
||||
|
||||
|
||||
def _configured_framing(configured: Mapping[str, object], default: Mapping[str, object], *keys: str) -> int | None:
|
||||
operator_sets_one: Final = _first_framing(configured, (*keys, "token_count_framing")) is not None
|
||||
return _first_framing(configured if operator_sets_one else default, keys)
|
||||
|
||||
|
||||
def _non_blank(value: object) -> str | None:
|
||||
return value if isinstance(value, str) and value.strip() else None
|
||||
|
||||
|
|
@ -175,13 +200,21 @@ def _tokenizer(value: object) -> TokenCountTokenizer | None:
|
|||
def resolve_token_count_settings(
|
||||
configured: Mapping[str, object], default: Mapping[str, object], deployment_region: Callable[[], str]
|
||||
) -> TokenCountSettings:
|
||||
framing: Final = _framing(configured.get("token_count_framing"))
|
||||
return TokenCountSettings(
|
||||
framing=framing if framing is not None else _framing(default.get("token_count_framing")),
|
||||
framing=_configured_framing(configured, default, "token_count_framing"),
|
||||
count_model=_resolved_count_model(
|
||||
configured.get("token_count_model"), default.get("token_count_model"), deployment_region
|
||||
),
|
||||
tokenizer=_tokenizer(default.get("token_count_tokenizer")),
|
||||
tool_framing=_configured_framing(configured, default, "token_count_tool_framing"),
|
||||
responses_framing=_configured_framing(configured, default, "token_count_responses_framing"),
|
||||
responses_tool_framing=_configured_framing(
|
||||
configured,
|
||||
default,
|
||||
"token_count_responses_tool_framing",
|
||||
"token_count_responses_framing",
|
||||
"token_count_tool_framing",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
172
litellm/proxy/dev_reload.py
Normal file
172
litellm/proxy/dev_reload.py
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import importlib
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from functools import partial
|
||||
from inspect import signature
|
||||
from multiprocessing.process import BaseProcess
|
||||
from socket import socket
|
||||
from typing import Final, Literal
|
||||
|
||||
from pydantic import ConfigDict, ImportString, JsonValue, TypeAdapter, with_config
|
||||
from typing_extensions import ReadOnly, Required, TypedDict
|
||||
from uvicorn import Config, Server
|
||||
from uvicorn import config as uvicorn_config
|
||||
from uvicorn.supervisors import ChangeReload, Multiprocess
|
||||
|
||||
|
||||
@with_config(ConfigDict(extra="allow"))
|
||||
class ServerOptions(TypedDict, total=False):
|
||||
host: ReadOnly[str]
|
||||
port: ReadOnly[int]
|
||||
server_header: ReadOnly[bool]
|
||||
timeout_graceful_shutdown: ReadOnly[float]
|
||||
log_config: ReadOnly[ # mutable-ok: uvicorn.Config requires a concrete logging dict
|
||||
str | dict[str, JsonValue] | None
|
||||
]
|
||||
log_level: ReadOnly[str | int]
|
||||
timeout_keep_alive: ReadOnly[int]
|
||||
timeout_worker_healthcheck: ReadOnly[int]
|
||||
limit_max_requests: ReadOnly[int]
|
||||
limit_max_requests_jitter: ReadOnly[int]
|
||||
limit_concurrency: ReadOnly[int]
|
||||
ssl_keyfile: ReadOnly[str]
|
||||
ssl_certfile: ReadOnly[str]
|
||||
loop: ReadOnly[Literal["none", "auto", "asyncio", "uvloop"]]
|
||||
|
||||
|
||||
@with_config(ConfigDict(extra="allow"))
|
||||
class ReloadOptions(ServerOptions):
|
||||
app: Required[ReadOnly[str]]
|
||||
reload: ReadOnly[bool]
|
||||
reload_dirs: ReadOnly[tuple[str, ...]]
|
||||
reload_includes: ReadOnly[tuple[str, ...]]
|
||||
|
||||
|
||||
RELOAD_OPTIONS: Final = TypeAdapter(ReloadOptions)
|
||||
SERVER_OPTIONS: Final = TypeAdapter(ServerOptions)
|
||||
ASGI_APP: Final = TypeAdapter(ImportString[Callable[..., object]])
|
||||
LOGGER: Final = logging.getLogger("uvicorn.error")
|
||||
RELOAD_WORKER_HEALTHCHECK_SECONDS: Final = 30
|
||||
RELOADER_ONLY_KEYS: Final = frozenset(ReloadOptions.__annotations__) - frozenset(ServerOptions.__annotations__)
|
||||
|
||||
|
||||
def multi_worker_reload_supported(platform: str = sys.platform) -> bool:
|
||||
return platform != "win32" and hasattr(uvicorn_config, "STARTUP_FAILURE")
|
||||
|
||||
|
||||
def server_options(options: ReloadOptions) -> ServerOptions:
|
||||
return SERVER_OPTIONS.validate_python(
|
||||
{key: value for key, value in options.items() if key not in RELOADER_ONLY_KEYS}
|
||||
)
|
||||
|
||||
|
||||
def load_app_or_exit(import_path: str, factory: bool = False) -> Callable[..., object]:
|
||||
module_name: Final = import_path.partition(":")[0]
|
||||
try:
|
||||
importlib.import_module(module_name)
|
||||
app: Final = ASGI_APP.validate_python(import_path)
|
||||
return ASGI_APP.validate_python(app()) if factory else app
|
||||
except Exception:
|
||||
LOGGER.exception("Error loading ASGI app %s", import_path)
|
||||
sys.exit(uvicorn_config.STARTUP_FAILURE)
|
||||
|
||||
|
||||
def stop_worker_when_pool_exits(pool_parent: BaseProcess) -> None:
|
||||
pool_parent.join()
|
||||
LOGGER.info("Worker pool process [%s] exited, stopping worker [%s].", pool_parent.pid, os.getpid())
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
|
||||
|
||||
def load_worker_app(import_path: str, worker_app_factory: bool) -> Callable[..., object]:
|
||||
pool_parent: Final = multiprocessing.parent_process()
|
||||
if pool_parent is not None:
|
||||
threading.Thread(target=stop_worker_when_pool_exits, args=(pool_parent,), daemon=True).start()
|
||||
return load_app_or_exit(import_path, worker_app_factory)
|
||||
|
||||
|
||||
def pool_config(options: ReloadOptions, workers: int, worker_overrides: Mapping[str, object] | None = None) -> Config:
|
||||
overrides: Final = worker_overrides or {}
|
||||
worker_app: Final = TypeAdapter(str).validate_python(overrides.get("app", options["app"]))
|
||||
worker_app_factory: Final = TypeAdapter(bool).validate_python(overrides.get("factory", False))
|
||||
return Config(
|
||||
partial(load_worker_app, worker_app, worker_app_factory),
|
||||
factory=True,
|
||||
workers=workers,
|
||||
**SERVER_OPTIONS.validate_python(
|
||||
{
|
||||
"timeout_worker_healthcheck": RELOAD_WORKER_HEALTHCHECK_SECONDS,
|
||||
**server_options(options),
|
||||
**{key: value for key, value in overrides.items() if key not in ("app", "factory")},
|
||||
}
|
||||
), # pyright: ignore[reportArgumentType] # uvicorn types the float graceful shutdown timeout as int
|
||||
)
|
||||
|
||||
|
||||
def stop_pool_when_reloader_exits(reloader: BaseProcess, pool_should_exit: threading.Event) -> None:
|
||||
reloader.join()
|
||||
LOGGER.info("Reloader process [%s] exited, stopping the worker pool.", reloader.pid)
|
||||
pool_should_exit.set()
|
||||
|
||||
|
||||
class ReloadWorkerPool(Multiprocess):
|
||||
quit_received: bool = False
|
||||
|
||||
def handle_quit(self) -> None:
|
||||
LOGGER.info("Received SIGQUIT, exiting.")
|
||||
self.quit_received = True
|
||||
self.should_exit.set()
|
||||
|
||||
def terminate_all(self) -> None:
|
||||
quit_signal: Final = getattr(signal, "SIGQUIT", None)
|
||||
if quit_signal is not None and quit_signal in self.signal_queue:
|
||||
self.handle_quit()
|
||||
if not self.quit_received:
|
||||
super().terminate_all()
|
||||
return
|
||||
for process in self.processes:
|
||||
process.kill()
|
||||
|
||||
|
||||
def _run_worker_pool(config: Config, sockets: Sequence[socket] | None) -> None:
|
||||
shared_sockets: Final = list(sockets or ())
|
||||
pool: Final = (
|
||||
ReloadWorkerPool(
|
||||
config=config,
|
||||
target=Server(config=config).run, # pyright: ignore[reportCallIssue] # Older uvicorn versions require target
|
||||
sockets=shared_sockets,
|
||||
)
|
||||
if "target" in signature(Multiprocess).parameters
|
||||
else ReloadWorkerPool(config=config, sockets=shared_sockets)
|
||||
)
|
||||
reloader: Final = multiprocessing.parent_process()
|
||||
if reloader is not None:
|
||||
threading.Thread(target=stop_pool_when_reloader_exits, args=(reloader, pool.should_exit), daemon=True).start()
|
||||
pool.run()
|
||||
|
||||
|
||||
def run_multi_worker_reload(
|
||||
options: ReloadOptions, workers: int, worker_overrides: Mapping[str, object] | None = None
|
||||
) -> None:
|
||||
worker_config: Final = pool_config(options, workers, worker_overrides)
|
||||
reload_config: Final = Config(
|
||||
options["app"],
|
||||
workers=1,
|
||||
reload=options["reload"],
|
||||
reload_dirs=list(options["reload_dirs"]),
|
||||
reload_includes=list(options["reload_includes"]),
|
||||
**server_options(options), # pyright: ignore[reportArgumentType] # uvicorn types the float graceful shutdown timeout as int
|
||||
)
|
||||
shared_socket: Final = worker_config.bind_socket()
|
||||
try:
|
||||
ChangeReload(
|
||||
config=reload_config,
|
||||
target=partial(_run_worker_pool, worker_config),
|
||||
sockets=[shared_socket],
|
||||
).run()
|
||||
finally:
|
||||
shared_socket.close()
|
||||
|
|
@ -9,7 +9,7 @@ import os
|
|||
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
||||
from datetime import datetime, timezone
|
||||
from types import MappingProxyType, UnionType
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, Protocol, TypeVar, Union, cast, get_args, get_origin
|
||||
from typing import TYPE_CHECKING, Any, Final, Protocol, TypeVar, Union, cast, get_args, get_origin
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
|
|
@ -43,6 +43,11 @@ from litellm.proxy.guardrails.guardrail_registry import (
|
|||
encrypt_guardrail_litellm_params,
|
||||
parse_tolerant_litellm_params,
|
||||
)
|
||||
from litellm.proxy.guardrails.response_handlers import (
|
||||
GUARDRAIL_RESPONSE_HANDLERS,
|
||||
GuardrailEndpointResponse,
|
||||
StandardGuardrailResponseHandler,
|
||||
)
|
||||
from litellm.proxy.guardrails.usage_endpoints import router as guardrails_usage_router
|
||||
from litellm.proxy.management_endpoints.common_utils import ( # noqa: F401 # legacy module exports
|
||||
_user_has_admin_view, # pyright: ignore[reportPrivateUsage,reportUnusedImport] # backwards-compatible package export
|
||||
|
|
@ -53,7 +58,6 @@ from litellm.repositories.table_repositories import GuardrailsRepository
|
|||
from litellm.types.guardrails import (
|
||||
PII_ENTITY_CATEGORIES_MAP,
|
||||
ApplyGuardrailRequest,
|
||||
ApplyGuardrailResponse,
|
||||
BaseLitellmParams,
|
||||
BedrockGuardrailConfigModel,
|
||||
BedrockGuardrailStreamingParams,
|
||||
|
|
@ -2350,13 +2354,7 @@ async def _run_test_guardrail(execute_guardrail: Callable[[], object], timeout:
|
|||
return await await_with_timeout(raw_result, remaining, label="test")
|
||||
|
||||
|
||||
def _resolve_guardrail_input_type(active_guardrail: CustomGuardrail, input_type: str) -> Literal["request", "response"]:
|
||||
"""Return the effective input_type, auto-upgrading to 'response' for post_call guardrails."""
|
||||
if input_type == "request":
|
||||
hook: Final = getattr(active_guardrail, "event_hook", None)
|
||||
if hook == GuardrailEventHooks.post_call or hook == "post_call":
|
||||
return "response"
|
||||
return "response" if input_type == "response" else "request"
|
||||
_RESPONSE: Final[TypeAdapter[GuardrailEndpointResponse]] = TypeAdapter(GuardrailEndpointResponse)
|
||||
|
||||
|
||||
class _GuardrailLoggingObj(Protocol):
|
||||
|
|
@ -2384,35 +2382,43 @@ def _patch_logging_obj_for_guardrail(litellm_logging_obj: _GuardrailLoggingObj,
|
|||
)
|
||||
|
||||
|
||||
async def _emit_guardrail_success_logs(
|
||||
async def _guardrail_response_after_success_hook(
|
||||
response_handler: StandardGuardrailResponseHandler,
|
||||
proxy_logging_obj: _GuardrailProxyLogging,
|
||||
litellm_logging_obj: _GuardrailLoggingObj | None,
|
||||
data: dict,
|
||||
data: Mapping[str, object],
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
response: ApplyGuardrailResponse,
|
||||
start_time: datetime,
|
||||
) -> ApplyGuardrailResponse:
|
||||
"""Fire proxy and LiteLLM success hooks after a successful guardrail run.
|
||||
|
||||
Each hook is wrapped defensively so a callback failure never prevents the
|
||||
caller from receiving the guardrail response. Returns the (possibly
|
||||
hook-modified) response.
|
||||
"""
|
||||
response: GuardrailEndpointResponse,
|
||||
) -> GuardrailEndpointResponse:
|
||||
try:
|
||||
modified: Final = await proxy_logging_obj.post_call_success_hook(
|
||||
data=data,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
response=response,
|
||||
)
|
||||
if isinstance(modified, ApplyGuardrailResponse):
|
||||
response = modified
|
||||
return response_handler.accept_hook_result(response, modified)
|
||||
except Exception:
|
||||
verbose_proxy_logger.exception("apply_guardrail: post_call_success_hook failed")
|
||||
return response
|
||||
|
||||
|
||||
async def _emit_guardrail_success_logs(
|
||||
response_handler: StandardGuardrailResponseHandler,
|
||||
proxy_logging_obj: _GuardrailProxyLogging,
|
||||
litellm_logging_obj: _GuardrailLoggingObj | None,
|
||||
data: Mapping[str, object],
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
response: GuardrailEndpointResponse,
|
||||
start_time: datetime,
|
||||
) -> GuardrailEndpointResponse:
|
||||
"""Apply the success hook and log the response returned to the caller."""
|
||||
updated_response: Final = await _guardrail_response_after_success_hook(
|
||||
response_handler, proxy_logging_obj, data, user_api_key_dict, response
|
||||
)
|
||||
|
||||
# Build the logging payload after post_call_success_hook so that logged
|
||||
# data matches what the caller actually receives if the hook modified
|
||||
# the response.
|
||||
response_for_logging: Final = {"response": response.model_dump(exclude_none=True)}
|
||||
response_for_logging: Final = {"response": _RESPONSE.dump_python(updated_response, mode="json")}
|
||||
|
||||
if litellm_logging_obj is not None:
|
||||
end_time: Final = datetime.now(timezone.utc)
|
||||
|
|
@ -2426,11 +2432,11 @@ async def _emit_guardrail_success_logs(
|
|||
except Exception:
|
||||
verbose_proxy_logger.exception("apply_guardrail: async_success_handler failed")
|
||||
|
||||
return response
|
||||
return updated_response
|
||||
|
||||
|
||||
@router.post("/guardrails/apply_guardrail", response_model=ApplyGuardrailResponse)
|
||||
@router.post("/apply_guardrail", response_model=ApplyGuardrailResponse)
|
||||
@router.post("/guardrails/apply_guardrail", response_model=GuardrailEndpointResponse)
|
||||
@router.post("/apply_guardrail", response_model=GuardrailEndpointResponse)
|
||||
async def apply_guardrail(
|
||||
fastapi_request: Request,
|
||||
request: ApplyGuardrailRequest,
|
||||
|
|
@ -2455,7 +2461,7 @@ async def apply_guardrail(
|
|||
)
|
||||
from litellm.proxy.utils import handle_exception_on_proxy
|
||||
|
||||
data: dict = {
|
||||
data: dict[str, object] = {
|
||||
"guardrail_name": request.guardrail_name,
|
||||
"input": [request.text],
|
||||
"messages": request.messages or [],
|
||||
|
|
@ -2495,18 +2501,15 @@ async def apply_guardrail(
|
|||
if litellm_logging_obj is not None:
|
||||
_patch_logging_obj_for_guardrail(litellm_logging_obj, request)
|
||||
|
||||
request_data: Final[dict] = {
|
||||
**({"messages": request.messages} if request.messages is not None else {}),
|
||||
**({"metadata": request.metadata} if request.metadata is not None else {}),
|
||||
}
|
||||
_input_type: Final = _resolve_guardrail_input_type(active_guardrail, request.input_type)
|
||||
response_handler: Final = GUARDRAIL_RESPONSE_HANDLERS[request.response_format]
|
||||
request_data: Final = dict(response_handler.prepare_request_data(request))
|
||||
input_type: Final = response_handler.resolve_input_type(request, active_guardrail)
|
||||
guardrailed_inputs: Final = await active_guardrail.apply_guardrail(
|
||||
inputs={"texts": [request.text]},
|
||||
request_data=request_data,
|
||||
input_type=_input_type,
|
||||
input_type=input_type,
|
||||
)
|
||||
response_text: Final = guardrailed_inputs.get("texts", [])
|
||||
response = ApplyGuardrailResponse(response_text=response_text[0] if response_text else request.text)
|
||||
response: Final = response_handler.build_response(request, guardrailed_inputs, request_data)
|
||||
except Exception as e:
|
||||
if litellm_logging_obj is not None and not isinstance(e, HTTPException):
|
||||
try:
|
||||
|
|
@ -2537,7 +2540,8 @@ async def apply_guardrail(
|
|||
raise handle_exception_on_proxy(status_typed_proxy_exception(e) if isinstance(e, HTTPException) else e)
|
||||
|
||||
# Success logging outside except so a hook error never triggers failure handlers.
|
||||
response = await _emit_guardrail_success_logs(
|
||||
logged_response: Final = await _emit_guardrail_success_logs(
|
||||
response_handler=response_handler,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
litellm_logging_obj=litellm_logging_obj,
|
||||
data=data,
|
||||
|
|
@ -2545,7 +2549,7 @@ async def apply_guardrail(
|
|||
response=response,
|
||||
start_time=start_time,
|
||||
)
|
||||
return response
|
||||
return logged_response
|
||||
|
||||
|
||||
# Usage (dashboard) endpoints: overview, detail, logs
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ from litellm.proxy.guardrails.anthropic_sse import (
|
|||
)
|
||||
from litellm.types.guardrails import (
|
||||
BedrockChecksConfigModel,
|
||||
BedrockGuardrailOutputScope,
|
||||
BedrockGuardrailStreamingParams,
|
||||
GuardrailEventHooks,
|
||||
LitellmParams,
|
||||
|
|
@ -289,6 +290,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
|
|||
streaming_buffer_release_on_scan: bool | None = None,
|
||||
streaming_scan_in_background: bool | None = None,
|
||||
async_handler: AsyncHTTPHandler | None = None,
|
||||
outputScope: BedrockGuardrailOutputScope | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
self.async_handler = async_handler or get_async_httpx_client(
|
||||
|
|
@ -309,6 +311,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
|
|||
)
|
||||
self.guardrailIdentifier = guardrailIdentifier
|
||||
self.guardrailVersion = guardrailVersion
|
||||
self.outputScope = outputScope
|
||||
self.guardrail_provider = "bedrock"
|
||||
self.chunk_budget_chars = chunk_budget_chars
|
||||
self.contextual_grounding_from_messages = contextual_grounding_from_messages
|
||||
|
|
@ -1280,10 +1283,10 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
|
|||
AWS billed them to ``completed_chunk_usages``, and the attempt log sums those
|
||||
with the blocking call's own usage.
|
||||
"""
|
||||
bedrock_request_data: Final = {
|
||||
**base_request_data,
|
||||
"content": content,
|
||||
}
|
||||
bedrock_request_data: Final = dict(base_request_data)
|
||||
bedrock_request_data["content"] = content
|
||||
if self.outputScope is not None and "outputScope" not in bedrock_request_data:
|
||||
bedrock_request_data["outputScope"] = self.outputScope.value
|
||||
prepared_request: Final = await run_aws_signing(
|
||||
self._prepare_request,
|
||||
credentials=credentials,
|
||||
|
|
|
|||
|
|
@ -261,7 +261,8 @@ class ResponsesStreamRestorer:
|
|||
own window, keyed by the family, the item id and the part index.
|
||||
- Any `*.done` event closes the stream of the same family. Whatever its window still
|
||||
holds goes out first, as a copy of that stream's last delta event -- so it carries
|
||||
the stream's own ids, and repeats that event's `sequence_number`. Then every text
|
||||
the stream's own ids. It takes the done event's `sequence_number`, and every later
|
||||
event's number moves up by one, so the numbers stay unique and in order. Then every text
|
||||
field on the done event is restored in full: its string fields other than
|
||||
identifiers, plus any `part` or `item` it repeats.
|
||||
- `response.completed` / `response.incomplete` repeat the whole reply, and are
|
||||
|
|
@ -273,6 +274,8 @@ class ResponsesStreamRestorer:
|
|||
self._rehydrate: Final = rehydrate
|
||||
self._carries: Final[dict[ResponsesStreamKey, str]] = {} # mutable-ok: per-stream windows advanced in place.
|
||||
self._last_deltas: Final[dict[ResponsesStreamKey, object]] = {} # mutable-ok: newest delta per stream.
|
||||
self._inserted: int = 0
|
||||
self._last_sequence_number: int | None = None
|
||||
|
||||
async def restore(self, event: object) -> tuple[object, ...]:
|
||||
"""The events to emit in place of `event`: any flush, then the event itself."""
|
||||
|
|
@ -281,7 +284,7 @@ class ResponsesStreamRestorer:
|
|||
return (event,)
|
||||
if kind.endswith(".delta") and kind not in RESPONSES_BINARY_DELTAS:
|
||||
await self._restore_delta(event, kind)
|
||||
return (event,)
|
||||
return self._numbered((), event)
|
||||
slots: Final[SlotSink] = []
|
||||
flushed: Final = await self._flush(responses_stream_key(event, kind)) if kind.endswith(".done") else ()
|
||||
if kind.endswith(".done"):
|
||||
|
|
@ -294,12 +297,29 @@ class ResponsesStreamRestorer:
|
|||
for item in read_list(read_field(event, "response"), "output"):
|
||||
collect_response_item(item, slots)
|
||||
await rehydrate_slots(slots, self._rehydrate)
|
||||
return (*flushed, event)
|
||||
return self._numbered(flushed, event)
|
||||
|
||||
async def finish(self) -> tuple[object, ...]:
|
||||
"""Flushes every stream the provider never closed, e.g. a truncated reply."""
|
||||
flushed: Final = tuple([await self._flush(key) for key in tuple(self._carries)])
|
||||
return tuple(itertools.chain.from_iterable(flushed))
|
||||
flushed: Final = tuple(itertools.chain.from_iterable([await self._flush(key) for key in tuple(self._carries)]))
|
||||
last: Final = self._last_sequence_number
|
||||
if last is not None:
|
||||
for offset, flush in enumerate(flushed, start=1):
|
||||
write_field(flush, "sequence_number", last + offset)
|
||||
return flushed
|
||||
|
||||
def _numbered(self, flushed: tuple[object, ...], event: object) -> tuple[object, ...]:
|
||||
"""Gives each flush the slot `event` arrived in and moves `event` and later events up."""
|
||||
number: Final = read_field(event, "sequence_number")
|
||||
if not isinstance(number, int):
|
||||
return (*flushed, event)
|
||||
for offset, flush in enumerate(flushed):
|
||||
write_field(flush, "sequence_number", number + self._inserted + offset)
|
||||
self._inserted += len(flushed)
|
||||
self._last_sequence_number = number + self._inserted
|
||||
if self._inserted:
|
||||
write_field(event, "sequence_number", self._last_sequence_number)
|
||||
return (*flushed, event)
|
||||
|
||||
async def _restore_delta(self, event: object, kind: str) -> None:
|
||||
text: Final = read_field(event, "delta")
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue