Skip to content

feat(together_ai): capability baseline, reasoning controls, and a current model map - #37745

Closed
zainhas wants to merge 9 commits into
BerriAI:litellm_internal_stagingfrom
zainhas:litellm_together_ai_provider_parity
Closed

zainhas wants to merge 9 commits into
BerriAI:litellm_internal_stagingfrom
zainhas:litellm_together_ai_provider_parity

Conversation

@zainhas

@zainhas zainhas commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Unmapped Together models silently lost tool calling
  • Structured outputs dropped alongside them
  • Reasoning controls never reached any Together model
  • Mapped per-model rates were shadowed by parameter-count buckets
  • Cache hits with no published discount billed as free
  • Reasoning tokens billed twice on some usage shapes
  • Calls went to the legacy api.together.xyz host

How it solves it:

  • Provider-level capability baseline, like fireworks_ai has
  • Reasoning, logprobs and max_tokens mapped to Together's shapes
  • Size buckets now price only what the map does not
  • A cache hit falls back to the standard input rate
  • Overlapping completion token details no longer double-count
  • Catalog refreshed, with cached-input rates
  • Default base URL is now the documented api.together.ai

User Flow

Before: a developer building an agent on a model Together launched this week gets no tool calls back, cannot turn thinking off, and sees the wrong spend

  1. They add together_ai/moonshotai/Kimi-K3 to their proxy config and restart
  2. They send POST http://localhost:4000/v1/chat/completions with that model, a tools array holding one get_weather function, and "What is the weather in Paris? Use the tool."
  3. The reply is prose, "I don't have access to a weather tool or any real-time data sources", with no tool_calls array, so their agent loop has nothing to execute
  4. On a proxy without drop_params the same request fails outright with 500 together_ai does not support parameters: ['tools']
  5. They switch to together_ai/Qwen/Qwen3.5-9B and send "reasoning_effort": "none" to skip thinking on a simple question; the answer still arrives behind 869 characters of chain of thought, and they pay for those tokens
  6. They send a 4.7k-token prompt to Kimi K3 twice and read the x-litellm-response-cost response header: it is empty both times, so the calls land in their dashboard at zero spend
  7. They repeat the same prompt against together_ai/meta-llama/Llama-3.3-70B-Instruct-Turbo and the header reads 0.0042876 every time, which is $0.90 per 1M tokens rather than the $1.04 Together publishes, and the response usage carries no cached-token count for their cache-hit dashboard
  8. They stream from together_ai/Qwen/Qwen3.7-Plus, which returns 140 completion tokens, and the spend recorded for that turn reflects 274 output tokens

After: the same requests return tool calls, thinking is controllable, and the spend numbers match Together's published rates

  1. They add together_ai/moonshotai/Kimi-K3 to their proxy config and restart
  2. They send POST http://localhost:4000/v1/chat/completions with that model, a tools array holding one get_weather function, and "What is the weather in Paris? Use the tool."
  3. The reply comes back with finish_reason: "tool_calls" and a tool_calls array naming get_weather with {"location": "Paris"}, so the agent loop runs
  4. "response_format": {"type": "json_schema", ...} on the same model returns schema-shaped JSON instead of a 500
  5. On together_ai/Qwen/Qwen3.5-9B, "reasoning_effort": "none" returns the answer with no chain of thought at all, and the same request without it still thinks
  6. They send a 4.7k-token prompt to Kimi K3 twice: the first call's x-litellm-response-cost reads 0.0141912 and the second, hitting Together's warm prefix, reads 0.0015444, a 9x drop that shows up in their dashboard
  7. The same prompt against Llama 3.3 70B Instruct Turbo reads 0.00495456, matching Together's published $1.04 per 1M, and the response usage now reports prompt_tokens_details.cached_tokens so their cache-hit dashboard is no longer stuck at zero
  8. Streaming from together_ai/Qwen/Qwen3.7-Plus charges the 140 completion tokens the model actually produced

Relevant issues

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. 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
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to 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

Both sides ran against the same live Together AI account, one proxy per side, with LITELLM_LOCAL_MODEL_COST_MAP=True so the bundled map is the one under test.

qa_config.yaml:

model_list:
  - model_name: kimi-k3
    litellm_params:
      model: together_ai/moonshotai/Kimi-K3
      api_key: os.environ/TOGETHER_API_KEY
  - model_name: llama-3.3-70b
    litellm_params:
      model: together_ai/meta-llama/Llama-3.3-70B-Instruct-Turbo
      api_key: os.environ/TOGETHER_API_KEY
  - model_name: qwen3.5-9b
    litellm_params:
      model: together_ai/Qwen/Qwen3.5-9B
      api_key: os.environ/TOGETHER_API_KEY

litellm_settings:
  drop_params: True
  telemetry: False

general_settings:
  master_key: sk-1234
LITELLM_LOCAL_MODEL_COST_MAP=True python litellm/proxy/proxy_cli.py --config qa_config.yaml --port 4010

Cases 3 and 4 send a 4.7k-token prefix twice so Together's automatic prefix cache can warm between calls. Cache hits are best-effort, so which call lands warm varies.

Before (66a89f5)

Tool calling on a model the cost map does not know

  1. curl -sS localhost:4010/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d @case1_tools.json | jq -c '{finish_reason: .choices[0].finish_reason, tool_calls: .choices[0].message.tool_calls, content_head: (.choices[0].message.content // "" | .[0:80])}'
  2. {"finish_reason":"stop","tool_calls":null,"content_head":"I don't have access to a weather tool or any real-time data sources, so I'm unab"}

Turning thinking off on a hybrid model

  1. curl -sS localhost:4010/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"model":"qwen3.5-9b","messages":[{"role":"user","content":"Is 9.11 bigger than 9.9? Answer in one short sentence."}],"reasoning_effort":"none","max_tokens":600}' | jq -c '{reasoning_chars: (.choices[0].message.reasoning_content // "" | length), content_head: (.choices[0].message.content // "" | .[0:60])}'
  2. {"reasoning_chars":869,"content_head":"No, 9.11 is smaller than 9.9."}

Cost and cached input on Kimi K3

  1. for i in 1 2; do curl -sS -D h.txt localhost:4010/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d @case3_cache.json | jq -c .usage; grep -i '^x-litellm-response-cost:' h.txt; done
  2. call 1 (cold): x-litellm-response-cost: (empty) with {"completion_tokens":8,"prompt_tokens":4748,"total_tokens":4756,"prompt_tokens_details":{"cached_tokens":64}}
  3. call 2 (warm): x-litellm-response-cost: (empty) with {"completion_tokens":8,"prompt_tokens":4748,"total_tokens":4756,"prompt_tokens_details":{"cached_tokens":4748}}

Published rate and flat cached tokens on Llama 3.3 70B

  1. for i in 1 2; do curl -sS -D h.txt localhost:4010/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d @case4_flat.json | jq -c '{usage, cached_via_details: .usage.prompt_tokens_details.cached_tokens}'; grep -i '^x-litellm-response-cost:' h.txt; done
  2. call 1: x-litellm-response-cost: 0.0042876 with {"usage":{"completion_tokens":2,"prompt_tokens":4762,"total_tokens":4764,"cached_tokens":0},"cached_via_details":null}
  3. call 2: x-litellm-response-cost: 0.0042876 with {"usage":{"completion_tokens":2,"prompt_tokens":4762,"total_tokens":4764,"cached_tokens":0},"cached_via_details":null}

After (b94e4a7)

Tool calling on a model the cost map does not know

  1. curl -sS localhost:4010/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d @case1_tools.json | jq -c '{finish_reason: .choices[0].finish_reason, tool_calls: .choices[0].message.tool_calls, content_head: (.choices[0].message.content // "" | .[0:80])}'
  2. {"finish_reason":"tool_calls","tool_calls":[{"function":{"arguments":"{\"location\":\"Paris\"}","name":"get_weather"},"id":"get_weather_0","type":"function"}],"content_head":"I'll check the current weather in Paris for you."}

Turning thinking off on a hybrid model

  1. curl -sS localhost:4010/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"model":"qwen3.5-9b","messages":[{"role":"user","content":"Is 9.11 bigger than 9.9? Answer in one short sentence."}],"reasoning_effort":"none","max_tokens":600}' | jq -c '{reasoning_chars: (.choices[0].message.reasoning_content // "" | length), content_head: (.choices[0].message.content // "" | .[0:60])}'
  2. {"reasoning_chars":0,"content_head":"No, 9.9 is bigger than 9.11."}

Cost and cached input on Kimi K3

  1. for i in 1 2; do curl -sS -D h.txt localhost:4010/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d @case3_cache.json | jq -c .usage; grep -i '^x-litellm-response-cost:' h.txt; done
  2. call 1 (cold): x-litellm-response-cost: 0.014191200000000001 with {"completion_tokens":8,"prompt_tokens":4748,"total_tokens":4756,"prompt_tokens_details":{"cached_tokens":64}}
  3. call 2 (warm): x-litellm-response-cost: 0.0015444 with {"completion_tokens":8,"prompt_tokens":4748,"total_tokens":4756,"prompt_tokens_details":{"cached_tokens":4748}}

Published rate and flat cached tokens on Llama 3.3 70B

  1. for i in 1 2; do curl -sS -D h.txt localhost:4010/v1/chat/completions -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d @case4_flat.json | jq -c '{usage, cached_via_details: .usage.prompt_tokens_details.cached_tokens}'; grep -i '^x-litellm-response-cost:' h.txt; done
  2. call 1: x-litellm-response-cost: 0.00495456 with {"usage":{"completion_tokens":2,"prompt_tokens":4762,"total_tokens":4764,"prompt_tokens_details":{"cached_tokens":4608},"cached_tokens":4608},"cached_via_details":4608}
  3. call 2: x-litellm-response-cost: 0.00495456 with {"usage":{"completion_tokens":2,"prompt_tokens":4762,"total_tokens":4764,"prompt_tokens_details":{"cached_tokens":4608},"cached_tokens":4608},"cached_via_details":4608}

Llama 3.3 70B publishes no cached-input discount, so a warm call costs the same as a cold one. What changes is the rate, $1.04 per 1M rather than the bucket's $0.90, and the cached count reaching prompt_tokens_details. The prefix was cold on both Before calls and warm on both After calls, which the counts show.

Every model this branch adds also got its own live call: 21 of 24 answered, and each one billed at exactly its mapped rate, which is the check that the size buckets no longer shadow the entries. The three that did not answer are provider-side rather than litellm: Kimi-K2.6 is listed but not serverless on this account, LFM2.5-8B-A1B returns 503 and no longer appears in /v1/models at all, and the pre-existing Qwen3-235B-A22B-Instruct-2507-tput entry whose output rate this branch corrects is retired from serverless. Qwen3.6-Plus, Qwen3.7-Plus and Qwen3.7-Max are streaming-only and were confirmed over a stream.

The reasoning double-count is the one fix with no proxy-visible before and after, because a streamed response flushes its headers before the usage chunk arrives, so x-litellm-response-cost is empty for streams on both sides. It shows up three other ways. On the wire, curl -N https://api.together.ai/v1/chat/completions -d '{"model":"Qwen/Qwen3.7-Plus", ..., "stream":true, "stream_options":{"include_usage":true}}' returns "completion_tokens":140 beside "completion_tokens_details":{"reasoning_tokens":134,"text_tokens":140}, where Qwen3.7 Max sends the disjoint shape instead. In the per-model sweep, both Plus models went from a mismatched computed cost to a matching one. And the arithmetic is pinned by a unit test.

Type

🆕 New Feature
🐛 Bug Fix
🧹 Refactoring

Caveats (if any)

  • basedpyright not run locally; left to CI
  • Qwen3.6/3.7 Plus and 3.7 Max are streaming-only
  • Kimi-K2.6 needs a dedicated endpoint on some accounts
  • Llama 3.3 70B and Gemma 4 31B spend rises to published rates
  • Cache-rate fallback touches every provider, raising previously free hits
  • api.together.xyz still works; only the default moved
  • Rerank untouched, still ignores a custom api base
  • max_output_tokens omitted where Together publishes no cap

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

get_provider_model_info was a 16-branch elif chain, and every provider added to it
pushed the function further past ruff's complexity ceiling. The ten providers whose
model info is just a zero-arg config constructor now come from a lazy factory table,
leaving explicit branches only for the cases that need a deferred import or the model
name. Registers together_ai on the way through, so the provider can serve a capability
baseline the way fireworks_ai already does.
Usage already understood DeepSeek's prompt_cache_hit_tokens and Anthropic's
cache_read_input_tokens, but not a plain cached_tokens sitting at the top level of
usage, which is what Together AI returns on its non-reasoning models. The count never
reached prompt_tokens_details, so the cost calculator billed a warm prefix at the full
input rate. The nested prompt_tokens_details value stays authoritative when a provider
sends both.
…rent model map

Together shipped models faster than litellm's cost map could follow, and the provider
read an absent map entry as "no tool calling": every call to a model the map had not
caught up with lost tools, tool_choice, function_call and response_format, with only a
debug line to explain it. TogetherAIConfig now serves a provider-level capability
baseline, the way fireworks_ai does, so an unmapped or brand-new id keeps Together's
real feature set while an explicit map entry still wins.

Reasoning controls now work: reasoning_effort and thinking reach the models that
support them, reasoning_effort="none" becomes Together's reasoning={"enabled": false}
toggle, "minimal" lands on "low", and the Anthropic-shaped thinking param maps onto the
same toggle. OpenAI's boolean logprobs plus top_logprobs collapse into the integer
logprobs Together documents, and max_completion_tokens becomes max_tokens instead of
being silently ignored. The Together-native params (top_k, min_p, repetition_penalty,
echo, context_length_exceeded_behavior, safety_model, chat_template_kwargs, reasoning)
are now advertised as supported rather than only tolerated.

The model map gains the current serverless catalog with cached-input rates, so cache
hits bill at the cached rate: Kimi K3, Kimi K2.7 Code, Kimi K2.6, GLM-5.2,
DeepSeek-V4-Pro and V4-Pro-0813 and V4-Flash-0731, MiniMax M3, Nemotron 3 Ultra,
Inkling and Inkling Small, the Qwen3.5/3.6/3.7/3.8 line, Gemma 4 31B, Cogito v2.1,
LFM2.5, Muse Glimmer, Ternary Bonsai, and the multilingual-e5 embedding model. Existing
entries stay put; Llama 3.3 70B Turbo and Qwen2.5 7B Turbo pick up their published rates
and context windows, and Qwen3-235B-A22B-Instruct-2507-tput no longer bills output at
ten times its published rate.

Rounding it out: /v1/models discovery so `litellm.get_valid_models` can enumerate an
account's models, one source of truth for the api base and the four accepted key names,
and the embeddings column flipped on in the support matrix, which the provider has
routed since main.py:6218.
@zainhas
zainhas requested a review from mateo-berri as a code owner August 21, 2026 00:37
@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR expands Together AI capability detection, parameter translation, model discovery, cached-token accounting, and the current pricing catalog

  • Adds data-driven tool, schema, reasoning, vision, and prompt-caching capability reporting
  • Translates Together-specific reasoning, token-limit, and logprobs request shapes
  • Normalizes flat cached-token usage for cost calculation
  • Refreshes Together model pricing and endpoint metadata

Confidence Score: 4/5

The PR appears safe to merge, with only non-blocking cleanup needed for redundant source comments

The changed provider translation, discovery, capability, and cached-token paths have focused regression coverage, and no concrete blocking failure remains

Files Needing Attention: litellm/types/utils.py, litellm/llms/together_ai/chat.py, litellm/utils.py

Important Files Changed

Filename Overview
litellm/llms/together_ai/chat.py Adds Together capability resolution, request translations, credential resolution, and model discovery; no blocking behavioral defect was established
litellm/types/utils.py Normalizes flat cached-token usage while preserving nested values, but adds explanatory comments contrary to repository guidance
litellm/utils.py Registers Together as a provider model-info factory and adds bundled pricing lookup support
model_prices_and_context_window.json Refreshes Together model capabilities, context limits, cache rates, and token pricing
litellm/model_prices_and_context_window_backup.json Keeps the bundled backup pricing catalog synchronized with the canonical Together model updates
tests/test_litellm/llms/together_ai/test_together_ai_chat_transformation.py Covers capability resolution, parameter translations, API configuration, and both supported model-discovery payload shapes
tests/test_litellm/llms/together_ai/test_together_ai_cost_calculator.py Verifies cached-prefix billing and representative updated Together catalog rates

Reviews (1): Last reviewed commit: "feat(together_ai): capability baseline, ..." | Re-trigger Greptile

Comment thread litellm/types/utils.py
Comment on lines +1766 to +1770
## FLAT `cached_tokens` MAPPING ##
# Some providers report cache hits at the top level of `usage` instead of nesting them
# under `prompt_tokens_details` (Together AI does this on its non-reasoning models), which
# would otherwise bill a cached prefix at the full input rate. The nested count is the more
# specific signal, so it wins when both are present.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant cache-mapping commentary

This prose restates the adjacent cached-token precedence condition and must be maintained alongside the implementation; keep comments only where complex business logic genuinely requires explanation. The same pattern appears in the new Together parameter helpers and model-info factory.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.70130% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...llm/litellm_core_utils/prompt_templates/factory.py 0.00% 1 Missing ⚠️
litellm/types/utils.py 75.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

get_model_params_and_category rewrote every together_ai model name into a parameter-count
bucket before the cost lookup ran, so any model whose name carries a parameter count billed
at its bucket rather than its own entry: Llama 3.3 70B Instruct Turbo charged the
41.1b-80b bucket's $0.90/1M against a published $1.04/1M, and Gemma 4 31B, Qwen3.5 9B,
LFM2.5 8B, Muse Glimmer 30B and Ternary Bonsai 27B were all mispriced the same way, with
their cached-input rates unreachable. The buckets now apply only to ids the map does not
price, which is what the neighbouring replicate branch already does.
reasoning_effort="none" and the Anthropic-shaped thinking param both map onto Together's
`reasoning` object, which is not an OpenAI parameter: handed to the SDK as a keyword it
raised "AsyncCompletions.create() got an unexpected keyword argument 'reasoning'" before
the request left the process. It now travels in extra_body, which the SDK spreads back
into the request body, and merges with an extra_body the caller already set.
…published

A missing cache_read_input_token_cost read as 0.0, so every cache hit on a model with no
published cached-input rate was billed as free. Together AI reports a cache hit on every
warm prefix, including on the models that publish no discount, so a repeated prompt there
tracked at a fraction of what it costs. Cached tokens now fall back to the standard input
rate, which is what the tiered-pricing path in the same module already does, and a
published cached rate still wins.
@codspeed

codspeed Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing zainhas:litellm_together_ai_provider_parity (b94e4a7) with litellm_internal_staging (65b4ac0)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (4b29702) during the generation of this report, so eb6296e was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Together documents https://api.together.ai/v1, and its own SDK and cURL examples use it;
litellm defaulted to api.together.xyz, the legacy host, and hardcoded it in the rerank
endpoint and the chat-template lookup too. Both hosts answer today, so api.together.xyz
stays a recognized openai-compatible endpoint and an explicit api_base pointing there is
still honored.
…t the wire body

The module-scoped fixture wrote litellm.model_cost directly, which is a process-wide
global the test-quality gate counts (TQ005); monkeypatch.setattr undoes it at teardown
and drops the hand-rolled save and restore. The two boundary tests now fake at the HTTP
layer and assert the JSON body Together would receive, rather than the keyword arguments
handed to the SDK, so they read the bytes on the wire and no longer restate the call.
…e total

Together AI reports completion_tokens_details with text_tokens equal to completion_tokens
and reasoning_tokens nested inside it on Qwen3.6 Plus and Qwen3.7 Plus, while Qwen3.7 Max
sends the disjoint shape. Text, audio, reasoning, image and video are each billed
separately, so the overlapping shape charged the reasoning tokens a second time: a
140-token completion billed as 274, about 1.96x. The remainder branch now also runs when
reasoning overlaps text, which is the completion-side twin of the cached-token guard the
prompt side already had.
@zainhas
zainhas marked this pull request as draft August 21, 2026 03:46
@thethaibinh

Copy link
Copy Markdown

Independent confirmation that this is still broken on v1.98.0 and on main (1.99.0), from a production deployment.

Reproduction

from litellm.utils import supports_function_calling

supports_function_calling("zai-org/GLM-5.2", custom_llm_provider="together_ai")                 # False
supports_function_calling("deepseek-ai/DeepSeek-V4-Flash-0731", custom_llm_provider="together_ai")  # False
supports_function_calling("Qwen/Qwen3.7-Plus", custom_llm_provider="together_ai")               # False

None are in model_cost, so _get_model_info_helper yields False rather than unknown. TogetherAIConfig.get_supported_openai_params then hits if supports_fc is not True and removes response_format along with the tool params.

Why this is worse than a missing capability

With the documented default drop_params: true, the parameter is discarded silently. Together then returns prose, the call reports success, and nothing surfaces a problem. Measured 8/8 across two models, against a strict json_schema response format.

Calling Together directly with the identical response_format returns valid JSON both times, so the capability is present and only the mapping is wrong.

With drop_params: false it correctly raises UnsupportedParamsError, which is how we found it.

For anyone hitting this before this PR lands: set drop_params: false. It converts a silent wrong answer into an immediate error.

Also worth noting that #18185 reported this in December 2025 and was closed with no comment and no fix.

Your framing matches what we found exactly: fireworks_ai has a provider-level baseline and passes response_format through correctly, together_ai has none. Happy to test a branch against real Together traffic if that is useful.

@mateo-berri

Copy link
Copy Markdown
Contributor

Closing: the dedicated Together chat config landed in #38248; reasoning controls and the model map follow in queued PRs. Thanks for contributing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants