Repository navigation
feat(sail): add Sail as a provider with service_tier mapped to its completion window - #42840
Conversation
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
There was a problem hiding this comment.
Devin Review found 1 potential issue.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
a1c52e9 to
c5ef3af
Compare
b7090eb to
fc17f4f
Compare
ab1fed1 to
37c6218
Compare
37c6218 to
b358482
Compare
b358482 to
be09b12
Compare
|
Generated by Claude Code |
|
bugbot run Generated by Claude Code |
|
Generated by Claude Code |
|
bugbot run Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit db710f9. Configure here.
…mpletion window Register Sail (providers.json, LlmProviders.SAIL, OpenAI-compatible lists, ProviderConfigManager) for chat, Responses and /v1/messages, and add its 12 models to both cost maps with asap, balanced and flex price columns. Sail picks speed and price with metadata.completion_window and rejects service_tier, so the Sail chat and Responses configs translate the tier: default and priority to asap, flex to flex, balanced to balanced, auto to no window. Billing prices the window that was sent. A tier Sail has no window for, or a window or tier set where billing cannot see it (request metadata, extra_body), is a 400 unless drop_params is set. Add balanced to ServiceTier and its _balanced price columns to the model info types, the Rust catalog and the dashboard schema. A transform_extra_body hook on the chat and Responses base configs, which returns extra_body unchanged by default, lets Sail keep the window when a caller also sends extra_body.metadata. Sail is listed in the Add Model form and model picker.
|
@greptileai please re-review the full PR diff against main. The last pass compared across a rebase and counted main's commits as this PR Generated by Claude Code |
TLDR
Problem this solves:
sail/<model>is not a known provider, so every Sail request failsmetadata.completion_window, which LiteLLM has no way to set or billHow it solves it:
/v1/messagesservice_tierto Sail's window:flexto flex,balancedto balanced,defaultandpriorityto asapdrop_params)Intentional product change:
balancedis a newservice_tiervalue, and model info output such as/model/infogains three price fields (input_cost_per_token_balanced,output_cost_per_token_balanced,cache_read_input_token_cost_balanced) that are null for every non-Sail modelUser Flow
Before: a proxy admin adds a Sail model and every request to it fails
model: sail/zai-org/GLM-5.3withapi_key: os.environ/SAIL_API_KEYto the proxy config and restart"model": "sail-glm"and"service_tier": "flex"sailproviderAfter: the same request runs on Sail's flex window and is billed at Sail's flex price
model: sail/zai-org/GLM-5.3withapi_key: os.environ/SAIL_API_KEYto the proxy config and restart"model": "sail-glm"and"service_tier": "flex"x-litellm-response-costequals the flex price times the returned token counts"balanced"bills the balanced price and no tier bills the asap price, the same way on POST /v1/responses"service_tier": "bogus"gets a 400 from LiteLLM that names the supported tiers, and nothing reaches SailDesign decisions
service_tieris the only knob, because it is the field LiteLLM's billing readsdefaultandprioritysendasap, since Sail has nothing fasterautosends no window, so Sail uses its own default, which is asapFLEXworks likeflexservice_tiergets a 400 from LiteLLM before Sail is called"bogus"and non-strings like5drop_paramsthe value is dropped and the call runs and bills at asap/v1/responsesa caller may also setmetadata.completion_windowdirectly, becausemetadatathere is a real OpenAI field that reaches Sailstandard, Sail's alias for balanced, is accepted and billed as balancedservice_tieris a 400/v1/chat/completionsa window inmetadataorextra_body.metadatais a 400service_tier, so that window would run at one price and bill at anotherservice_tierinsideextra_bodyis a 400 on both endpoints, for the same reasonextra_body.metadatakeys are kept and merged with the window instead of replacing it/v1/messagespasses through like every other provider on that path, soservice_tieris ignored therestop,seed,frequency_penalty,presence_penalty,logit_bias,logprobsandtop_logprobs, so LiteLLM rejects them first anddrop_paramscan strip themmax_tokensandmax_completion_tokenspass through as sent, since Sail accepts and enforces both (checked live below)balancedjoins the sharedServiceTierenum with_balancedprice columns; only Sail rows carry them, so every other model prices exactly as beforeLinear ticket
Resolves LIT-8503
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Every call below hits the real Sail API through a local proxy, run on 2026-09-26. The proxy config has two deployments on
sail/zai-org/GLM-5.3:sail-glm, andsail-glm-dropwithdrop_params: true. Both useapi_key: os.environ/SAIL_API_KEY. The proxy was started withLITELLM_MODE=PRODUCTION LITELLM_LOCAL_MODEL_COST_MAP=True python litellm/proxy/proxy_cli.py --config sail.yaml --port 4001, and$LITELLM_MASTER_KEYis a random local keyCosts are checked by hand against the
sail/zai-org/GLM-5.3row: asap (base) 9.8e-07 input, 1.8e-07 cached, 3.08e-06 output; balanced 5e-07, 1.2e-07, 2.5e-06; flex 4e-07, 8e-08, 1.8e-06Before (e065a25)
curl -si localhost:4001/v1/chat/completions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" -d '{"model":"sail-glm","messages":[{"role":"user","content":"Say hi in five words."}],"max_tokens":64,"service_tier":"flex"}'HTTP/1.1 400 Bad Request,litellm.BadRequestError: You passed in model=sail-glm. There are no healthy deployments for this model, because main does not know thesailproviderAfter (c38a38e)
chat, no service_tier
curl -si localhost:4001/v1/chat/completions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" -d '{"model":"sail-glm","messages":[{"role":"user","content":"Say hi in five words."}],"max_tokens":256}'HTTP/1.1 200 OK,x-litellm-response-cost: 0.00080612, usage 18 prompt (0 cached), 256 completionchat, service_tier flex
curl -si localhost:4001/v1/chat/completions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" -d '{"model":"sail-glm","messages":[{"role":"user","content":"Say hi in five words."}],"max_tokens":256,"service_tier":"flex"}'HTTP/1.1 200 OK,x-litellm-response-cost: 0.0004122, usage 18 prompt (0 cached), 225 completionchat, service_tier balanced
curl -si localhost:4001/v1/chat/completions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" -d '{"model":"sail-glm","messages":[{"role":"user","content":"Say hi in five words."}],"max_tokens":256,"service_tier":"balanced"}'HTTP/1.1 200 OK,x-litellm-response-cost: 0.00050754, usage 18 prompt (17 cached), 202 completionchat, unknown service_tier
curl -si localhost:4001/v1/chat/completions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" -d '{"model":"sail-glm","messages":[{"role":"user","content":"Say hi in five words."}],"max_tokens":64,"service_tier":"bogus"}'HTTP/1.1 400 Bad Request,x-litellm-response-cost: 0,litellm.UnsupportedParamsError: sail does not support service_tier='bogus'. Supported values: auto, default, priority, flex, balanced.chat, non-string service_tier
curl -si localhost:4001/v1/chat/completions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" -d '{"model":"sail-glm","messages":[{"role":"user","content":"Say hi in five words."}],"max_tokens":64,"service_tier":5}'HTTP/1.1 400 Bad Request,litellm.UnsupportedParamsError: sail does not support service_tier=5. Supported values: auto, default, priority, flex, balanced.chat, unknown service_tier under drop_params
curl -si localhost:4001/v1/chat/completions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" -d '{"model":"sail-glm-drop","messages":[{"role":"user","content":"Say hi in five words."}],"max_tokens":256,"service_tier":"bogus"}'HTTP/1.1 200 OK,x-litellm-response-cost: 0.00079252, usage 18 prompt (17 cached), 256 completionchat, a param Sail rejects
curl -si localhost:4001/v1/chat/completions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" -d '{"model":"sail-glm","messages":[{"role":"user","content":"Say hi in five words."}],"max_tokens":64,"stop":["x"]}'HTTP/1.1 400 Bad Request,litellm.UnsupportedParamsError: sail does not support parameters: ['stop'], for model=zai-org/GLM-5.3chat, max_tokens passes through
curl -si localhost:4001/v1/chat/completions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" -d '{"model":"sail-glm","messages":[{"role":"user","content":"Write a 200 word story about a cat."}],"max_tokens":16}'HTTP/1.1 200 OK, usage 16 completion tokens,finish_reason: length, so Sail enforced the limitresponses, metadata.completion_window flex
curl -si localhost:4001/v1/responses -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" -d '{"model":"sail-glm","input":"Say hi in five words.","max_output_tokens":256,"metadata":{"completion_window":"flex"}}'HTTP/1.1 200 OK,x-litellm-response-cost: 0.00028256, usage 18 input (17 cached), 156 output, and Sail echoesmetadata.completion_window: flexresponses, service_tier balanced
curl -si localhost:4001/v1/responses -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" -d '{"model":"sail-glm","input":"Say hi in five words.","max_output_tokens":256,"service_tier":"balanced"}'HTTP/1.1 200 OK,x-litellm-response-cost: 0.00064254, usage 18 input (17 cached), 256 output, and Sail echoesmetadata.completion_window: standard, its internal name for balancedmessages, plain
curl -si localhost:4001/v1/messages -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" -H "anthropic-version: 2023-06-01" -d '{"model":"sail-glm","max_tokens":256,"messages":[{"role":"user","content":"Say hi in five words."}]}'HTTP/1.1 200 OK,x-litellm-response-cost: 0.00080612, usage 18 input, 256 outputmessages, service_tier flex is ignored
curl -si localhost:4001/v1/messages -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" -H "anthropic-version: 2023-06-01" -d '{"model":"sail-glm","max_tokens":256,"service_tier":"flex","messages":[{"role":"user","content":"Say hi in five words."}]}'HTTP/1.1 200 OK,x-litellm-response-cost: 0.00068292, usage 18 input, 216 outputType
🆕 New Feature
Caveats (if any)
Medium
service_tieron/v1/messagesis ignored, like for every provider on that pathlitellm.completion_cost(completion_response=resp)called by hand on a flex chat response prices it at asapx-litellm-response-costheader are right; follow-up is LIT-8651Low
sail/openai/gpt-oss-120bhas one price andsail/Qwen/Qwen3.6-35B-A3Bhas no balanced price on Sail's pricing page (checked 2026-09-25), so missing tiers bill the base pricetransform_extra_bodyhook on the chat and Responses base configsextra_bodyunchanged, and only Sail overrides itQA runbook
Prerequisites: a proxy with
SAIL_API_KEYset,STORE_MODEL_IN_DB=Trueand Postgres for the spend rows. The tests pick the cheapestsail/row whose asap, balanced and flex prices all differ from/model/info, and register their own deployment through/model/new{"model_name": "sail-e2e", "litellm_params": {"model": "sail/zai-org/GLM-5.3", "api_key": "os.environ/SAIL_API_KEY"}}"model": "sail-e2e"three times:"service_tier": "flex","balanced","auto"x-litellm-response-costequal to (prompt - cached) x input + cached x cache read + completion x output at that tier's columns, and the spend row equal to the header"service_tier": "bogus"service_tier"model": "sail-e2e","input": "hi","metadata": {"completion_window": "flex"}/v1/messagesworks on Sail"model": "sail-e2e","max_tokens": 16Final Attestation