Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions charts/openhands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,28 @@ This Helm chart deploys the complete OpenHands stack, including all required dep
See the [values.yaml](values.yaml) file for the full list of configurable parameters.
Make sure to update all values marked with "REQUIRED" comments.

### Organization condenser defaults

Set `orgDefaults.condenser.maxTokens` to add a token-based condensation threshold for applicable OpenHands organization settings. Any positive integer is accepted. Condensation triggers on the smaller of this value and the agent LLM's effective input limit, so a value above that limit has no effect; it does not change the model context window, and event-count condensation may still occur first.

These values only take effect from the app version that reads the `OPENHANDS_ORG_DEFAULTS_CONDENSER_*` environment variables, which is tracked in [OpenHands/enterprise#337](https://github.com/OpenHands/enterprise/pull/337). The current chart `appVersion` does not include that reader, so on it these values render into the pod and do nothing.

```yaml
orgDefaults:
condenser:
maxTokens: 200000
applyToExisting: true
overwriteExisting: true
```

New applicable OpenHands org settings receive the configured value. Existing org rows are only updated when `applyToExisting: true`; existing non-null values are only replaced when `overwriteExisting: true`. Missing and JSON-null `condenser.max_tokens` values are treated as unset.

Reconciliation is triggered from the app server's startup lifespan, so it runs once per `saas_server` worker process every time one starts, and it stays armed for as long as these values remain in the release. The maintenance CronJobs that share this env block never trigger it, because they run standalone `python -m` entrypoints that do not start the app. A concurrent database lock serializes overlapping runs, so repeated runs converge on the same result rather than conflicting.

The practical consequence is that this is not a rollout-scoped, one-time operation. If `overwriteExisting: true` stays in a site values file, a pod restart, rollout, HPA scale-up, or node eviction re-applies it and overwrites org-admin UI changes made since the last start. Remove `applyToExisting`/`overwriteExisting` once the rollout completes. Then remove `orgDefaults.condenser.maxTokens` as well if you want to stop future defaulting and reconciliation.

`overwriteExisting: true` is destructive for prior org-level `max_tokens` values. Take a database backup before enabling it. Removing `orgDefaults.condenser.maxTokens` later does not restore previous org-specific values; restore from backup or run corrective SQL if rollback is required.

### Email (Resend)

To enable organization invitation emails via Resend, set `resend.enabled: true` and create a Kubernetes secret named `resend-api-key` with key `resend-api-key` containing your Resend API key. The secret name can be overridden with `resend.auth.existingSecret`.
Expand Down
19 changes: 19 additions & 0 deletions charts/openhands/templates/_env.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{{- define "openhands.env.defaults" }}
{{- $integrations := .Values.integrations | default dict }}
{{- $orgCondenser := (.Values.orgDefaults | default dict).condenser | default dict }}
- name: RUNTIME
value: remote
- name: OH_SANDBOX_SPEC_KIND
Expand Down Expand Up @@ -34,6 +35,24 @@
- name: OH_DAILY_CONVERSATION_LIMIT
value: {{ .Values.dailyConversationLimit | quote }}
{{- end }}
{{- /*
Only the app-server startup lifespan reads these, so the copies that land in
the maintenance CronJobs sharing this block are inert -- those run standalone
`python -m` entrypoints that never start the app.
*/}}
{{- if ne $orgCondenser.maxTokens nil }}
{{- /*
int64 before quoting: JSON Schema's integer type accepts YAML floats with a
zero fraction, and Go renders 1e6 as "1e+06", which the backend's isdecimal
parse rejects by raising out of the startup lifespan.
*/}}
- name: OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS
value: {{ $orgCondenser.maxTokens | int64 | quote }}
- name: OPENHANDS_ORG_DEFAULTS_CONDENSER_APPLY_TO_EXISTING
value: {{ $orgCondenser.applyToExisting | default false | quote }}
- name: OPENHANDS_ORG_DEFAULTS_CONDENSER_OVERWRITE_EXISTING
value: {{ $orgCondenser.overwriteExisting | default false | quote }}
{{- end }}
- name: DUPLICATE_EMAIL_CHECK
value: {{ .Values.duplicateEmailCheck | quote }}
- name: OH_RESOLVER_LABEL
Expand Down
7 changes: 7 additions & 0 deletions charts/openhands/templates/validations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ are never rendered, so a server-less release must not trip them.
{{- if and .Values.enabled .Values.enterpriseSSO.idpMetadataUrl (not (include "openhands.keycloakProvisionRealm" .)) -}}
{{- fail "enterpriseSSO.idpMetadataUrl requires chart-managed Keycloak realm provisioning. Set keycloak.enabled=true or keycloak.provisionRealm=true, or leave enterpriseSSO.idpMetadataUrl empty and manage the provider manually." -}}
{{- end -}}
{{- $orgCondenser := (.Values.orgDefaults | default dict).condenser | default dict -}}
{{- if and .Values.enabled (eq $orgCondenser.maxTokens nil) (or $orgCondenser.applyToExisting $orgCondenser.overwriteExisting) -}}
{{- fail "orgDefaults.condenser.maxTokens must be set before applyToExisting or overwriteExisting can be enabled." -}}
{{- end -}}
{{- if and .Values.enabled $orgCondenser.overwriteExisting (not $orgCondenser.applyToExisting) -}}
{{- fail "orgDefaults.condenser.overwriteExisting requires orgDefaults.condenser.applyToExisting=true because overwrite mode only runs while reconciling existing organizations." -}}
{{- end -}}

{{/*
Data-loss guard for the bundled MinIO. Its chart runs the bucket job on
Expand Down
121 changes: 121 additions & 0 deletions charts/openhands/tests/org_defaults_condenser_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
suite: org defaults condenser env wiring
templates:
- deployment.yaml
- litellm-config-script.yaml
tests:
- it: leaves org condenser env unset by default
set:
enabled: true
asserts:
- template: deployment.yaml
notExists:
path: spec.template.spec.containers[0].env[?(@.name=="OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS")]
- template: deployment.yaml
notExists:
path: spec.template.spec.containers[0].env[?(@.name=="OPENHANDS_ORG_DEFAULTS_CONDENSER_APPLY_TO_EXISTING")]
- template: deployment.yaml
notExists:
path: spec.template.spec.containers[0].env[?(@.name=="OPENHANDS_ORG_DEFAULTS_CONDENSER_OVERWRITE_EXISTING")]

- it: renders without org condenser env when the orgDefaults block is null
set:
enabled: true
orgDefaults: null
asserts:
- template: deployment.yaml
notExists:
path: spec.template.spec.containers[0].env[?(@.name=="OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS")]

- it: renders without org condenser env when the condenser block is null
set:
enabled: true
orgDefaults:
condenser: null
asserts:
- template: deployment.yaml
notExists:
path: spec.template.spec.containers[0].env[?(@.name=="OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS")]

- it: wires false rollout flags when only a threshold is configured
set:
enabled: true
orgDefaults.condenser.maxTokens: 200000
asserts:
- template: deployment.yaml
contains:
path: spec.template.spec.containers[0].env
content:
name: OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS
value: "200000"
- template: deployment.yaml
contains:
path: spec.template.spec.containers[0].env
content:
name: OPENHANDS_ORG_DEFAULTS_CONDENSER_APPLY_TO_EXISTING
value: "false"
- template: deployment.yaml
contains:
path: spec.template.spec.containers[0].env
content:
name: OPENHANDS_ORG_DEFAULTS_CONDENSER_OVERWRITE_EXISTING
value: "false"

- it: defaults null rollout flags to false rather than an empty value
set:
enabled: true
orgDefaults.condenser.maxTokens: 200000
orgDefaults.condenser.applyToExisting: null
orgDefaults.condenser.overwriteExisting: null
asserts:
- template: deployment.yaml
contains:
path: spec.template.spec.containers[0].env
content:
name: OPENHANDS_ORG_DEFAULTS_CONDENSER_APPLY_TO_EXISTING
value: "false"
- template: deployment.yaml
contains:
path: spec.template.spec.containers[0].env
content:
name: OPENHANDS_ORG_DEFAULTS_CONDENSER_OVERWRITE_EXISTING
value: "false"

- it: renders an exponent-notation threshold as a plain integer
set:
enabled: true
orgDefaults:
condenser:
maxTokens: 1.0e+6
asserts:
- template: deployment.yaml
contains:
path: spec.template.spec.containers[0].env
content:
name: OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS
value: "1000000"

- it: wires configured org condenser rollout settings
set:
enabled: true
orgDefaults.condenser.maxTokens: 200000
orgDefaults.condenser.applyToExisting: true
orgDefaults.condenser.overwriteExisting: true
asserts:
- template: deployment.yaml
contains:
path: spec.template.spec.containers[0].env
content:
name: OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS
value: "200000"
- template: deployment.yaml
contains:
path: spec.template.spec.containers[0].env
content:
name: OPENHANDS_ORG_DEFAULTS_CONDENSER_APPLY_TO_EXISTING
value: "true"
- template: deployment.yaml
contains:
path: spec.template.spec.containers[0].env
content:
name: OPENHANDS_ORG_DEFAULTS_CONDENSER_OVERWRITE_EXISTING
value: "true"
37 changes: 37 additions & 0 deletions charts/openhands/tests/validations_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,43 @@ tests:
- hasDocuments:
count: 0

- it: fails when org condenser rollout flags are set without a threshold
set:
orgDefaults.condenser.maxTokens: null
orgDefaults.condenser.applyToExisting: true
asserts:
- failedTemplate:
errorMessage: "orgDefaults.condenser.maxTokens must be set before applyToExisting or overwriteExisting can be enabled."
- it: fails when org condenser overwrite is enabled without applyToExisting
set:
orgDefaults.condenser.maxTokens: 200000
orgDefaults.condenser.applyToExisting: false
orgDefaults.condenser.overwriteExisting: true
asserts:
- failedTemplate:
errorMessage: "orgDefaults.condenser.overwriteExisting requires orgDefaults.condenser.applyToExisting=true because overwrite mode only runs while reconciling existing organizations."
- it: allows org condenser overwrite when existing-org reconciliation is enabled
set:
orgDefaults.condenser.maxTokens: 200000
orgDefaults.condenser.applyToExisting: true
orgDefaults.condenser.overwriteExisting: true
asserts:
- hasDocuments:
count: 0
- it: renders no resources when the orgDefaults block is null
set:
orgDefaults: null
asserts:
- hasDocuments:
count: 0
- it: renders no resources when the org condenser block is null
set:
orgDefaults:
condenser: null
asserts:
- hasDocuments:
count: 0

# The bundled MinIO bucket job runs on post-upgrade too, so purge: true wipes
# conversation and session data on every ordinary upgrade of a persistent
# install. These cases pin the guard that rejects it.
Expand Down
38 changes: 38 additions & 0 deletions charts/openhands/tests/values_schema_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,41 @@ tests:
asserts:
- failedTemplate:
errorPattern: "ingress/enabled.*want boolean"
- it: rejects orgDefaults.condenser.maxTokens below the minimum
set:
orgDefaults.condenser.maxTokens: 0
asserts:
- failedTemplate:
errorPattern: "orgDefaults/condenser/maxTokens"
- it: rejects negative orgDefaults.condenser.maxTokens
set:
orgDefaults.condenser.maxTokens: -1
asserts:
- failedTemplate:
errorPattern: "orgDefaults/condenser/maxTokens"
- it: accepts orgDefaults.condenser.maxTokens above one million
set:
orgDefaults.condenser.maxTokens: 2000000
asserts:
- notFailedTemplate: {}
- it: rejects a non-integer orgDefaults.condenser.maxTokens
set:
orgDefaults.condenser.maxTokens: "200000"
asserts:
- failedTemplate:
errorPattern: "orgDefaults/condenser/maxTokens"
- it: rejects a non-boolean orgDefaults.condenser.applyToExisting
set:
orgDefaults.condenser.applyToExisting: "true"
asserts:
- failedTemplate:
errorPattern: "orgDefaults/condenser/applyToExisting.*want boolean"
- it: rejects a non-boolean orgDefaults.condenser.overwriteExisting
set:
orgDefaults.condenser.overwriteExisting: "false"
asserts:
- failedTemplate:
errorPattern: "orgDefaults/condenser/overwriteExisting.*want boolean"
- it: rejects a non-integer daily conversation limit
set:
dailyConversationLimit: "twenty"
Expand Down Expand Up @@ -95,5 +130,8 @@ tests:
enterpriseSSO.idpMetadataUrl: https://idp.example.com/metadata
filestore.secretAccessKeyKey: secret-key
laminar.clickhouse.diagnostics.retentionDays: 2
orgDefaults.condenser.maxTokens: 200000
orgDefaults.condenser.applyToExisting: true
orgDefaults.condenser.overwriteExisting: false
asserts:
- notFailedTemplate: {}
13 changes: 13 additions & 0 deletions charts/openhands/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@
"enabled": { "type": "boolean" },
"dailyConversationLimit": { "type": ["integer", "null"] },
"duplicateEmailCheck": { "type": "boolean" },
"orgDefaults": {
"type": ["object", "null"],
"properties": {
"condenser": {
"type": ["object", "null"],
"properties": {
"maxTokens": { "type": ["integer", "null"], "minimum": 1 },
"applyToExisting": { "type": "boolean" },
"overwriteExisting": { "type": "boolean" }
}
}
}
},
"image": {
"type": "object",
"required": ["repository", "tag"],
Expand Down
17 changes: 17 additions & 0 deletions charts/openhands/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@ dailyConversationLimit: null
# a plus-address suffix (for example, name+test@example.com and name@example.com).
duplicateEmailCheck: true

# Organization-default condenser settings. These seed newly-created applicable
# OpenHands org settings. Existing org rows are only reconciled when explicitly
# enabled below because that mutates persisted database settings.
orgDefaults:
condenser:
# Conversation-history size that triggers condensation, NOT an LLM output-token
# cap. Named to match the backend's condenser.max_tokens field. Condensation
# fires on the smaller of this and the agent LLM's effective input limit, so a
# larger value has no effect. Positive integer, or null for app/SDK defaults.
maxTokens: null
# Mutate existing org rows during reconciliation, which the app server runs on
# every start while these values are set -- not just once per rollout.
applyToExisting: false
# Force existing non-null org-level values to maxTokens during reconciliation.
# Destructive and repeated on every app start; see README before enabling.
overwriteExisting: false

datadog:
enabled: false
env: "dev" # Default environment, can be overridden
Expand Down
Loading