* fix(proxy): persist sent model_info fields on POST /model/update
POST /model/update rebuilt litellm_params from the request but dropped the
request's model_info, so changing base_model (or mode, tier, etc.) on an
existing DB model was silently ignored. Clients that only use this endpoint,
like the Terraform provider, could never fix a deployment's base_model
without deleting and recreating it.
Persist the model_info fields the caller actually sent, on top of the stored
ones. ModelInfo defaults never overwrite stored values, server-owned, team,
PTU and pricing fields stay on their dedicated write paths, and values echoed
from a /model/info response are not pinned.
* fix(proxy): keep echoed and blank model_info values out of POST /model/update writes
Blank strings from typed clients no longer overwrite stored values. Values echoed from
/model/info (the stored base model's cost map entry and discovered limits) are not
pinned, and an echoed catalog value resets a differing stored override like PATCH does.
direct_access and access_via_team_ids stay display-only. The tests stop patching the
auth check class and give the team lookup a real row instead.
* fix(proxy): lock the model row and skip unchanged model_info on POST /model/update
* fix(proxy): treat only keyed /model/update bodies as echoes and write the stored model_info as saved
A typed model_info value that matched the cost map, or the previous base model's entry,
was dropped and could erase a stored override. Echo detection now follows PATCH, only a
body carrying the /model/info key counts, compared against the base model the row has
after the write. The locked write patches the stored JSON instead of dumping the hydrated
Deployment, so litellm_params pricing is no longer copied into model_info. Tests run the
real Router with discovery over a mock upstream
* docs(terraform): note that model_info updates now take effect and overwrite values set outside Terraform
* test: expect POST /model/update to save the cache point flag, and wrap long test lines
test_x06 pinned the old contract, where POST /model/update answered 200 and ignored model_info.
This PR makes POST save model_info the way PATCH does, so x06 now asserts what its PATCH sibling
x01 asserts, the stored flag flips to true and cache points come back on.
* fix(proxy): answer 400 when POST /model/update gets an access_groups that is not a list of strings
* fix(proxy): read discovered model info through the router registry entry point
---------
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
* fix(terraform): send additional_drop_params to the proxy instead of deleting the keys it names
litellm_model treated additional_drop_params in additional_litellm_params as a
provider-side delete list: it never sent the list and removed every key it named
from litellm_params, so vertex_location vanished and requests still carried
store. The list now goes to the proxy like any other JSON value, so the proxy
drops those params per request. Approach credited to #38345.
Fixes#38349
* fix(terraform)!: validate additional_drop_params and read it back from the proxy on refresh
* docs(terraform): note that a non-list additional_drop_params now fails the apply
* fix(terraform)!: check additional_drop_params at plan time and refresh listed keys and outside lists
* fix(terraform)!: show every listed key an older provider deleted in the upgrade plan and leave outside drop lists alone
Refresh no longer reads listed keys back from the proxy, which left a
plan diff that never cleared when a listed key was removed from config.
When the proxy has no stored list, the plan now shows the list and each
listed key the config sets as changes. A drop list set outside Terraform
on a model whose config has none stays out of the plan, and an empty
value now fails the plan
* fix(terraform): show a listed merge_reasoning_content_in_choices in the upgrade plan
The refresh now clears merge_reasoning_content_in_choices when it is listed and the proxy has no stored list, since an older provider deleted it, and no longer clears litellm_credential_name, which the older provider always sent
The proxy answers POST /v1/mcp/server with 201 and DELETE /v1/mcp/server/{id} with 202, but handleMCPAPIResponse, handleAPIResponse, Client.sendRequest and resourceLiteLLMMCPServerDelete treated every status other than 200 as a failure. terraform apply created the server on the proxy, then reported an error and never wrote the resource to state, so the next apply created a duplicate, and destroy failed on the 202.
Accept any 2xx in the four status checks, treat an empty or null 2xx body in handleAPIResponse as an empty ModelResponse, and cover each path with httptest tables that fail on the 200-only comparison.
Re-lands the diff from #40723 on current main.
Co-authored-by: matthew-hull-bright <matthew.hull@brightsg.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat(terraform): add team_member_model_max_budget to litellm_team
The proxy now accepts per-model budgets for team members, so a member can
be capped on one model and keep using the team's others. The provider had
no way to set or read it, so it could only be managed by hand.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix(terraform): tighten team_member_model_max_budget per review
Reject tpm_limit/rpm_limit at plan time since the proxy rejects them on team
budgets, skip sending an empty map on create so no empty member budget row
is made, and document the router-alias matching and minimum proxy version.
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
* fix(terraform): stop sending blocked on litellm_user updates
/user/update writes blocked into the user table upsert, but blocked only
exists as a column on keys, so every litellm_user update failed with a 400
from Prisma. Updates no longer send it. Create sends it only when set to
true, since /user/new applies it to the auto-created key
blocked only applies at creation; litellm_key_block covers changes after that
* fix(terraform): always send blocked on litellm_user create
/user/new fills an omitted blocked from default_internal_user_params, so
leaving it out on create let a proxy-side blocked=true default block the
auto-created key. Create sends the configured value again, including
false, while updates still never send it
* chore(terraform): drop comment on litellm_user create blocked
* fix(terraform): keep litellm_user blocked undeprecated
Create sends blocked again, so it still controls the auto-created key.
Whether to deprecate it in favor of litellm_key_block is left to the
provider maintainers; the docs describe it as creation-only instead
* fix(provider): accept 2xx status in handleResponse for access group create
handleResponse (resource_team.go) only accepted exactly HTTP 200, but
POST /v1/access_group (and its /v1/unified_access_group alias) legitimately
answers 201 Created. litellm_access_group and litellm_unified_access_group
create both succeeded on the proxy and failed in the provider, leaving the
group out of state and forcing an import to recover on the next apply's
409 for the now-duplicate name.
Same fix and shape as #40723, which widened this exact check in
sendRequest/handleAPIResponse/handleMCPAPIResponse for mcp_server, model,
key and organization_member. handleResponse is the one shared status-check
helper that fix didn't reach -- it's a different function in a different
file (resource_team.go, not client.go/utils.go), so this is fully
independent of that PR and can land before, after, or alongside it with
no conflict.
handleResponse is also used by agent, budget, guardrail, organization,
prompt, search_tool, tag, team, team_block, key_block, team_member(_add)
and user -- all unaffected in practice, since every one of their own
endpoints already answers exactly 200. Widening the check costs them
nothing and only changes behavior for the two resources that were
actually broken.
Verified: go test ./... passes, including a new
TestHandleResponseAcceptsFullSuccessRange table test covering
200/201/202/204 (accepted) and 400/404/409/500 (still rejected), mirroring
#40723's own TestHandleAPIResponseAcceptsFullSuccessRange.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: address Greptile review on PR 42461
- CHANGELOG: narrowed the fix's scope to unified_access_group only.
litellm_access_group (legacy) calls /access_group/new, a completely
different, unrelated endpoint (model_access_group_management_endpoints.py)
that already returns 200 -- it was never affected. I'd wrongly assumed
both resources shared the same /v1/access_group route; they don't.
- resource_team_test.go: removed the preamble comment above the new test,
which restated what the table test already shows -- against repository
guidance (AGENTS.md) that reserves comments for complex logic, tool
inputs, or TODOs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* chore: retrigger CI
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* feat(terraform): add display_name to litellm_model resource and model data sources
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(terraform): persist display_name on update and read /model/info data envelope
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* chore(terraform): drop PATCH /model/{model_id}/update from endpoint audit allowlist
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(terraform): surface external display_name removal as drift on refresh
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* ci(terraform): rerun after uv download timeout
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
A JWT key mapping can now name its virtual key by the SHA-256 hash the proxy
already stores, instead of only by the plaintext key.
litellm_key makes its generated key write-only so raw keys stay out of Terraform
state, and write-only attributes cannot be referenced at all, so the natural
wiring fails while planning, in every apply ordering:
Error: Missing required argument
with litellm_jwt_key_mapping.example
key = litellm_key.example.key
The argument "key" is required, but no definition was found.
The only way out today is supplying the plaintext from a variable or a secret
manager, which means the mapped key cannot be one the proxy generated and the
configuration has to carry a credential. The value the mapping stores is
hash_token(key), which is the same hash litellm_key already exports as
token_id, and a hash is not a credential, so accepting it closes the gap:
resource "litellm_jwt_key_mapping" "service" {
jwt_claim_name = "client_id"
jwt_claim_value = "reporting-service"
token_id = litellm_key.service.token_id
}
CreateJWTKeyMappingRequest and UpdateJWTKeyMappingRequest gain an optional
token. Create requires exactly one of key or token, update accepts at most one,
and omitting both still leaves the mapped key alone. A supplied token must be 64
lowercase hex characters, because hash_token() hashes unconditionally and a
plaintext key sent as token would be stored as a hash of a hash, then silently
match nothing at auth time. Both rejections are 400s raised before the row is
written.
On the provider side, key becomes Optional with ExactlyOneOf{key, token_id} and
token_id is added next to it. token_id is not marked sensitive since a hash is
not a credential, both fields are omitempty on the wire so the proxy receives
only the one that was configured, and a failed update reverts token_id for the
same reason it already reverts key.
key keeps working unchanged and existing state is untouched. The only change to
it is Required to Optional, which no existing configuration can violate.
budget_duration and allowed_models ride on /team/member_add. tpm_limit and rpm_limit are sent through /team/member_update, the only endpoint that accepts them. Removing any of the four from config sends an explicit clear (null, or an empty list for allowed_models) since member_update is a merge-patch. The resource ID is set before the post-add limits call so a failure there taints the resource instead of orphaning the memberships
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Terraform's convention is that create does not seize a resource the
configuration never made, and credential_values holds secrets that are never
read back into state, so a silent takeover overwrites values no plan showed.
A name collision now fails with the terraform import command that adopts the
existing credential explicitly, and adopt_existing = true opts into taking it
over during create. The provider detects the conflict by the proxy's 409 and
keeps the Prisma string match as a fallback for older proxies
Credential names and model_id went into URLs raw, so a name with a slash or a
question mark hit the wrong route. Every credential URL is now built from a
package const through fmt.Sprintf with url.PathEscape or url.QueryEscape,
which the endpoint audit can resolve. Toggling adopt_existing alone no longer
sends a PATCH, so it does not rewrite the stored secret
The proxy answered a duplicate credential_name with a 500 carrying Prisma's
unique-constraint message, so terraform apply against a credential the state
had lost died with an opaque database error. Classify that response as a
conflict, adopt the existing credential with a PATCH that carries model_id,
and set the resource ID only once the adopt succeeds so a failed PATCH does
not taint a credential this run never owned
Squashed from the commits on #39745 with authorship preserved
Fixes https://github.com/BerriAI/terraform-provider-litellm/issues/8
Deleting a team cascade-deletes its keys, so `terraform apply -replace` on a
team left the key's `/key/update` 404ing and aborted the apply with the key
resource stuck. The update now confirms the key is really gone and recreates it
under the new team; a `team_id` change between two live teams stays an in-place
update, and an unrelated failure still errors out.
Rebased onto current staging, which added a typed `apiError` and `isNotFound`,
so the recovery matches on the status code plus a re-read rather than on the
error string. The metadata pre-read, which fails before `/key/update` is ever
reached when the key is gone, routes through the same recovery.
Original work by @matthowardcohere in #39747.
Claude-Session: https://claude.ai/code/session_01XT1qsbjLwnhiN5sQ2hNUxr
* fix(terraform): send litellm_key model_max_budget as per-model BudgetConfig objects
The key resource typed model_max_budget as map(number) and forwarded the
bare numbers to /key/generate, which the proxy rejects with a 500
('int' object is not iterable) because each model entry must be a
BudgetConfig object. The attribute is now a JSON string of per-model
budget objects, matching litellm_user, litellm_budget and litellm_tag,
with a schema version 1 state upgrader that drops the old map value so
existing state keeps loading
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(terraform): reject litellm_key model_max_budget JSON that is not per-model budget objects
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(terraform): reject unknown or empty per-model budget fields in litellm_key model_max_budget
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Add an optional, computed, force-new team_id argument to the
litellm_team resource. When set, it is sent to /team/new as the
team_id; when omitted the provider keeps generating a UUID. Read
mirrors the resource id into state so import and plan stay clean.
Resolves LIT-6399
Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat(terraform): add litellm_jwt_key_mapping resource
Adds a Terraform resource for the proxy's JWT to virtual key mappings, so a
JWT client identified by a claim such as client_id, azp or sub maps to a
virtual key and inherits its models, budgets, rate limits and spend tracking.
Covers the four mapping endpoints: /jwt/key/mapping/new, /info, /update and
/delete. is_active is applied through a follow-up update because the create
endpoint always starts a mapping active, a dropped description is sent as an
empty string because the update endpoint ignores absent fields, changing the
mapped key rotates it in place, and changing the claim name or value forces
replacement since the update endpoint cannot change them.
* fix(terraform): revert key on failed jwt_key_mapping update
Classic SDKv2 persists a failed Update's diff-applied values to state
regardless of the error, so a rejected key rotation left the new key in
state while the proxy kept the old one and the next plan falsely converged.
Revert key via GetChange and resync description/is_active/computed fields
from a post-failure Read, since Read alone can't recover key (the proxy
never returns it).
Also drop the case-insensitive "mapping not found" body match: the proxy
raises 404 for all three not-found paths (info, update, delete), so
checking the status code alone is sufficient.
Clarify the docs: referencing a litellm_key resource's write-only key is
not a null-then-400 situation, it's a static "Missing required argument"
error at plan time, in every apply ordering.
* fix(terraform): stop leaving an active mapping behind on failed cleanup
Two issues flagged by review:
- Create has no way to ask the proxy for an inactive mapping, so an
is_active=false mapping is briefly active while the follow-up
deactivation runs. If that deactivation call itself fails, the mapping
used to stay active and untracked. It's now deleted instead, closing
the exposure rather than leaving it open indefinitely.
- On a failed update, only `key` was reverted before the recovery read.
If that read also failed, description/is_active kept the rejected
values, so a later plan could report false convergence. Now all three
are reverted before the read runs.
Both come with regression tests, mutation-verified against the pre-fix
code.
* fix(deps): bump restrictedpython to 8.5 for GHSA-ffg3-p8fm-mjx2
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* chore: retrigger ci
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(tests): stub anthropic judge credentials in funnel seeding test
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* revert(deps): keep uv.lock unchanged to keep the PR terraform-only
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: Fabrice Pont <fabrice.pont@doctolib.com>
Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat(terraform): close key/team schema gaps, fix team read envelope, add import support
* feat(terraform): add fallback resource/data source and key/team block resources
* feat(terraform): add access group and unified access group resources and data sources
* feat(terraform): add guardrail and prompt resources and data sources
* feat(terraform): add agent and search tool resources and data sources
* feat(terraform): add user and budget resources and data sources
* feat(terraform): add tag and project resources and data sources
* docs(terraform): changelog and readme for parity additions
* feat(terraform): add data sources for keys, teams, models, organizations, and mcp servers
* fix(terraform): key update 400 on empty budget_duration, key info envelope, config-supplied key
* fix(terraform): hash raw keys to SHA-256 tokens in key lookup URLs and block resource IDs
* fix(terraform): add soft_budget, tags, and soft_budget_alerting_emails to litellm_team
The team resource rejected soft_budget and tags at plan time and had no way
to express the list-valued metadata.soft_budget_alerting_emails the proxy
reads for soft-budget alerts, even though /team/new and /team/update accept
all three. Add the attributes, forward them in buildTeamData (alert emails
merged under metadata, where the proxy stores them), and send the full
metadata map whenever either half changes because /team/update replaces
metadata wholesale.
Read was decoding /team/info as if the team fields were top-level, but the
proxy nests them under team_info, so every attribute silently fell back to
prior state. Decode the envelope and split the proxy's metadata back into
tags / soft_budget_alerting_emails / string metadata, dropping the
server-managed team_member_budget_id.
Verified with OpenTofu plan/apply against a live proxy: the attributes are
accepted, land on the proxy, refresh into state, re-plan clean, propagate
on update, and clear when removed from HCL.
* fix(terraform): clear litellm_team.soft_budget in state when the proxy returns null
Read only wrote soft_budget when the proxy returned a value, so a soft
budget cleared outside Terraform stayed in state and never surfaced as
drift. Set it from the response unconditionally so a null clears it.
The provider is published in lockstep with LiteLLM: every dev, rc and stable
release mirrors terraform/provider/ from the release commit and tags it with
the LiteLLM version, alongside the aws/google modules. The 0.x line ends at
0.4.0, and the CHANGELOG headings no longer drive a release.
RELEASING.md describes the new flow and how to recover a version whose
goreleaser run failed; README gains a Versioning section with the re-pin
note for anyone on `~> 0.4`; CHANGELOG records the change under Unreleased.
goreleaser gets `prerelease: auto` so a v1.99.0-dev.1 / -rc.1 tag in the
mirror is marked as a pre-release instead of becoming the repo's latest
release. The registry ingests it either way.
The provider's release gate in project-releaser publishes only when the
topmost released heading in terraform/provider/CHANGELOG.md moves past the
tag the mirror already carries. That heading has been 0.2.2 since
2026-05-13, so every stable release since has correctly decided there was
nothing to publish and the registry has gone stale.
Two things were blocking a release:
1. The mirror shipped 0.3.0 out-of-band on 2026-07-13 (pricing_base_model,
BerriAI/terraform-provider-litellm#47) after the source move, so that
code exists only in the mirror. The publish rsyncs monorepo -> mirror
with --delete, so publishing without this port would have deleted a
released feature from the registry.
2. Nothing here declared a new version.
Port #47 verbatim (resource_model.go and resource_model_crud.go are now
byte-identical to the mirror's released files), backfill the 0.3.0
changelog entry it shipped under, and cut 0.4.0 covering the changes made
here since the source move. 0.3.0 is not reusable as the next version --
the mirror holds that tag and the publish workflow's tag guard rejects it.
* feat(terraform): vendor terraform-provider-litellm as source of truth with endpoint drift CI
* fix(terraform): address review feedback on vendored provider
Replace deprecated io/ioutil with io. Remove the unused org/team CRUD
client methods so the endpoint audit only tracks live call sites
(54 -> 46). Redact request/response logs by parsing the JSON and
recursively masking sensitive fields, which fixes the nested-object
leak in the old credential_values regex, with a regex fallback for
non-JSON payloads; covered by new unit tests. Docs: stop showing
api_key inside vector store litellm_params and document that Sensitive
attributes still persist in plaintext state, recommending
litellm_credential_name and an encrypted state backend.
* fix(terraform): stop persisting server-returned litellm_params into vector store state
The vector store Read wrote litellm_params straight back from the API
response into state. The proxy redacts secrets in those responses, so
the readback overwrote user config with redaction sentinels and caused
perpetual diffs, and against a server that returns raw values it would
persist secrets into a non-Sensitive attribute. Read now preserves the
config value like the credential and model resources do, litellm_params
is marked Sensitive, and a regression test pins that a server-returned
api_key never lands in state
* fix(terraform): send role on team member update and stop persisting server env into MCP state
The team member update payload omitted role, and the proxy leaves role
unchanged when the field is absent, so a role downgrade reported as
applied by Terraform never took effect on the proxy. The update now
always sends the configured role (the attribute is Required).
The MCP server resource wrote env straight back from API responses
into a non-Sensitive attribute, pulling admin-visible secrets into
state and, for sanitized responses, blanking user config. Read now
preserves the config value, env is marked Sensitive, and the docs warn
against passing secrets via args. Regression tests cover both fixes
and fail against the previous behavior.