Compare commits

..

12 Commits

Author SHA1 Message Date
Engel Nyst c8d856cfa5 tests(LLM): add concurrency and provider-mapping reinit tests; fix style
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-14 16:26:02 +00:00
Engel Nyst db5c0c687c style(tests): apply pre-commit formatting fixes
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-14 05:27:36 +00:00
Engel Nyst 82d5b0388b tests(LLM): add reinit tests (basic, capability recompute, cost flag reset)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-14 05:24:38 +00:00
Engel Nyst bf192688a5 LLM: add lock + reinit helper; recompute capabilities in centralized initializer; keep update_config alias
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-14 05:18:38 +00:00
Engel Nyst 477d9b17c0 Merge branch 'main' into llm-init to resolve conflicts
Co-authored-by: OpenHands-GPT-4.1 <openhands@all-hands.dev>
2025-08-14 03:31:27 +00:00
OpenHands Bot 6756427217 🤖 Auto-fix Python linting issues 2025-07-30 23:33:14 +00:00
Engel Nyst 1b74920509 Merge branch 'main' into llm-init 2025-07-31 01:31:31 +02:00
Engel Nyst c933b88f36 Merge branch 'main' into llm-init 2025-07-30 02:01:52 +02:00
Engel Nyst e0e9d3d07c Merge branch 'main' into llm-init 2025-07-26 22:50:17 +02:00
Engel Nyst 00f9ff08c7 Merge branch 'main' into llm-init 2025-07-22 00:53:46 +02:00
Engel Nyst f37f8fb723 Merge branch 'main' into llm-init 2025-07-21 01:57:07 +02:00
Engel Nyst 8628da0037 Refactor LLM class to support runtime configuration updates
- Decouple partial function creation from LLM initialization
- Extract _build_completion_function() method for reusable completion setup
- Add update_config() method for hot-swapping LLM configuration at runtime
- Add _rebuild_completion_wrapper() method to recreate retry decorator wrapper
- Preserve metrics and retry listener instances during config updates
- Handle model changes by resetting model info for re-initialization
- Support custom tokenizer updates and log completion folder creation
- Add comprehensive unit tests covering all update scenarios

This prepares the LLM class for the upcoming unified configuration system
that will enable runtime config reloading without restart.

Co-authored-by: OpenHands <openhands@all-hands.dev>
2025-07-19 15:07:52 +02:00
41 changed files with 614 additions and 335 deletions
+1 -1
View File
@@ -159,7 +159,7 @@ poetry run pytest ./tests/unit/test_*.py
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker
container image by setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.53-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.52-nikolaik`
## Develop inside Docker container
+3 -3
View File
@@ -79,17 +79,17 @@ You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)
You can also run OpenHands directly with Docker:
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.53
docker.all-hands.dev/all-hands-ai/openhands:0.52
```
</details>
+3 -3
View File
@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.53
docker.all-hands.dev/all-hands-ai/openhands:0.52
```
> **注意**: 如果您在0.44版本之前使用过OpenHands,您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。
+3 -3
View File
@@ -42,17 +42,17 @@ OpenHandsはDockerを利用してローカル環境でも実行できます。
> 公共ネットワークで実行していますか?[Hardened Docker Installation Guide](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)を参照して、ネットワークバインディングの制限や追加のセキュリティ対策を実施してください。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.53
docker.all-hands.dev/all-hands-ai/openhands:0.52
```
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。
+1 -1
View File
@@ -12,7 +12,7 @@ services:
- SANDBOX_API_HOSTNAME=host.docker.internal
- DOCKER_HOST_ADDR=host.docker.internal
#
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.53-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.52-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
+1 -1
View File
@@ -7,7 +7,7 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik}
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of ~/.openhands for this user
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

@@ -78,14 +78,6 @@ description: Complete guide for setting up Jira Data Center integration with Ope
- **Service Account API Key**: The personal access token from Step 2 above
- Ensure **Active** toggle is enabled
<Note>
Workspace name is the host name of your Jira Data Center instance.
Eg: http://jira.all-hands.dev/projects/OH/issues/OH-77
Here the workspace name is **jira.all-hands.dev**.
</Note>
3. **Complete OAuth Flow**
- You'll be redirected to Jira Data Center to complete OAuth verification
- Grant the necessary permissions to verify your workspace access. If you have access to multiple workspaces, select the correct one that you initially provided
@@ -109,18 +101,18 @@ Here the workspace name is **jira.all-hands.dev**.
<AccordionGroup>
<Accordion title="Workspace link flow">
![workspace-link.png](/static/img/jira-dc-user-link.png)
![workspace-link.png](/static/img/workspace-link.png)
</Accordion>
<Accordion title="Workspace Configure flow">
![workspace-link.png](/static/img/jira-dc-admin-configure.png)
![workspace-link.png](/static/img/workspace-configure.png)
</Accordion>
<Accordion title="Edit view as a user">
![workspace-link.png](/static/img/jira-dc-user-unlink.png)
![workspace-link.png](/static/img/workspace-user-edit.png)
</Accordion>
<Accordion title="Edit view as the workspace creator">
![workspace-link.png](/static/img/jira-dc-admin-edit.png)
![workspace-link.png](/static/img/workspace-admin-edit.png)
</Accordion>
</AccordionGroup>
@@ -15,27 +15,28 @@ description: Complete guide for setting up Jira Cloud integration with OpenHands
- Go to **Directory** > **Users**
2. **Create OpenHands Service Account**
- Click **Service accounts**
- Click **Create a service account**
- Name: `OpenHands Agent`
- Click **Next**
- Select **User** role for Jira app
- Click **Create**
- Click **Add user**
- Email: `openhands@yourcompany.com` (replace with your preferred service account email)
- Display name: `OpenHands Agent`
- Send invitation: **No** (you'll set password manually)
- Click **Add user**
3. **Configure Account**
- Locate the created user and click on it
- Set a secure password
- Add to relevant Jira projects with appropriate permissions
### Step 2: Generate API Token
1. **Access Service Account Configuration**
- Locate the created service account from above step and click on it
1. **Access API Token Management**
- Log in as the OpenHands service account
- Go to [API Tokens](https://id.atlassian.com/manage-profile/security/api-tokens)
2. **Create API Token**
- Click **Create API token**
- Set the expiry to 365 days (maximum allowed value)
- Click **Next**
- In **Select token scopes** screen, filter by following values
- App: Jira
- Scope type: Classic
- Scope actions: Write, Read
- Select `read:jira-work` and `write:jira-work` scopes
- Click **Next**
- Review and create API token
- Label: `OpenHands Cloud Integration`
- Expiry: Set appropriate expiration (recommend 1 year)
- Click **Create**
- **Important**: Copy and securely store the token immediately
### Step 3: Configure Webhook
@@ -82,14 +83,6 @@ description: Complete guide for setting up Jira Cloud integration with OpenHands
- **Service Account API Key**: The API token from Step 2 above
- Ensure **Active** toggle is enabled
<Note>
Workspace name is the host name when accessing a resource in Jira Cloud.
Eg: https://all-hands.atlassian.net/browse/OH-55
Here the workspace name is **all-hands**.
</Note>
3. **Complete OAuth Flow**
- You'll be redirected to Jira Cloud to complete OAuth verification
- Grant the necessary permissions to verify your workspace access.
@@ -113,18 +106,18 @@ Here the workspace name is **all-hands**.
<AccordionGroup>
<Accordion title="Workspace link flow">
![workspace-link.png](/static/img/jira-user-link.png)
![workspace-link.png](/static/img/workspace-link.png)
</Accordion>
<Accordion title="Workspace Configure flow">
![workspace-link.png](/static/img/jira-admin-configure.png)
![workspace-link.png](/static/img/workspace-configure.png)
</Accordion>
<Accordion title="Edit view as a user">
![workspace-link.png](/static/img/jira-user-unlink.png)
![workspace-link.png](/static/img/workspace-user-edit.png)
</Accordion>
<Accordion title="Edit view as the workspace creator">
![workspace-link.png](/static/img/jira-admin-edit.png)
![workspace-link.png](/static/img/workspace-admin-edit.png)
</Accordion>
</AccordionGroup>
@@ -28,7 +28,7 @@ description: Complete guide for setting up Linear integration with OpenHands Clo
1. **Access API Settings**
- Log in as the service account
- Go to **Settings** > **Security & access**
- Go to **Settings** > **API**
2. **Create Personal API Key**
- Click **Create new key**
@@ -82,14 +82,6 @@ description: Complete guide for setting up Linear integration with OpenHands Clo
- **Service Account API Key**: The API key from Step 2 above
- Ensure **Active** toggle is enabled
<Note>
Workspace name is the identifier after the host name when accessing a resource in Linear.
Eg: https://linear.app/allhands/issue/OH-37
Here the workspace name is **allhands**.
</Note>
3. **Complete OAuth Flow**
- You'll be redirected to Linear to complete OAuth verification
- Grant the necessary permissions to verify your workspace access. If you have access to multiple workspaces, select the correct one that you initially provided
@@ -113,15 +105,15 @@ Here the workspace name is **allhands**.
<AccordionGroup>
<Accordion title="Workspace link flow">
![workspace-link.png](/static/img/linear-user-link.png)
![workspace-link.png](/static/img/workspace-link.png)
</Accordion>
<Accordion title="Workspace Configure flow">
![workspace-link.png](/static/img/linear-admin-configure.png)
![workspace-link.png](/static/img/workspace-configure.png)
</Accordion>
<Accordion title="Edit view as a user">
![workspace-link.png](/static/img/linear-admin-edit.png)
![workspace-link.png](/static/img/workspace-user-edit.png)
</Accordion>
<Accordion title="Edit view as the workspace creator">
@@ -58,18 +58,17 @@ The OpenHands agent needs to identify which Git repository to work with when pro
### Platform Configuration Issues
- **Webhook not triggering**: Verify the webhook URL is correct and the proper event types are selected (Comment, Issue updated)
- **API authentication failing**: Check API key/token validity and ensure required scopes are granted. If your current API token is expired, make sure to update it in the respective integration settings
- **API authentication failing**: Check API key/token validity and ensure required scopes are granted
- **Permission errors**: Ensure the service account has access to relevant projects/teams and appropriate permissions
### Workspace Integration Issues
- **Workspace linking requests credentials**: If there are no active workspace integrations for the workspace you specified, you need to configure it first. Contact your platform administrator that you want to integrate with (eg: Jira, Linear)
- **OAuth flow fails**: Ensure you're signing in with the same Git provider account that contains the repositories you want OpenHands to work on
- **Integration not found**: Verify the workspace name matches exactly and that platform configuration was completed first
- **OAuth flow fails**: Make sure that you're authorizing with the correct account with proper workspace access
### General Issues
- **Agent not responding**: Check webhook logs in your platform settings and verify service account status
- **Authentication errors**: Verify Git provider permissions and OpenHands Cloud access
- **Agent fails to identify git repo**: Ensure you're signing in with the same Git provider account that contains the repositories you want OpenHands to work on
- **Partial functionality**: Ensure both platform configuration and workspace integration are properly completed
### Getting Help
+2 -2
View File
@@ -119,7 +119,7 @@ The conversation history will be saved in `~/.openhands/sessions`.
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -128,7 +128,7 @@ docker run -it \
-v ~/.openhands:/.openhands \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.53 \
docker.all-hands.dev/all-hands-ai/openhands:0.52 \
python -m openhands.cli.main --override-cli-mode true
```
+2 -2
View File
@@ -61,7 +61,7 @@ export GITHUB_TOKEN="your-token" # Required for repository operations
# Run OpenHands
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -73,7 +73,7 @@ docker run -it \
-v ~/.openhands:/.openhands \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.53 \
docker.all-hands.dev/all-hands-ai/openhands:0.52 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
+4 -4
View File
@@ -68,23 +68,23 @@ Download and install the LM Studio desktop app from [lmstudio.ai](https://lmstud
1. Check [the installation guide](/usage/local-setup) and ensure all prerequisites are met before running OpenHands, then run:
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.53
docker.all-hands.dev/all-hands-ai/openhands:0.52
```
2. Wait until the server is running (see log below):
```
Digest: sha256:e72f9baecb458aedb9afc2cd5bc935118d1868719e55d50da73190d3a85c674f
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.53
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.52
Starting OpenHands...
Running OpenHands as root
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None
+3 -3
View File
@@ -109,17 +109,17 @@ Note that you'll still need `uv` installed for the default MCP servers to work p
<Accordion title="Docker Command (Click to expand)">
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.53
docker.all-hands.dev/all-hands-ai/openhands:0.52
```
</Accordion>
+1 -60
View File
@@ -1,5 +1,5 @@
import { screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { describe, expect, it } from "vitest";
import i18n from "../../src/i18n";
import { AccountSettingsContextMenu } from "../../src/components/features/context-menu/account-settings-context-menu";
import { renderWithProviders } from "../../test-utils";
@@ -17,63 +17,4 @@ describe("Translations", () => {
screen.getByTestId("account-settings-context-menu"),
).toBeInTheDocument();
});
it("should not attempt to load unsupported language codes", async () => {
// Test that the configuration prevents 404 errors by not attempting to load
// unsupported language codes like 'en-US@posix'
const originalLanguage = i18n.language;
try {
// With nonExplicitSupportedLngs: false, i18next will not attempt to load
// unsupported language codes, preventing 404 errors
// Test with a language code that includes region but is not in supportedLngs
await i18n.changeLanguage("en-US@posix");
// Since "en-US@posix" is not in supportedLngs and nonExplicitSupportedLngs is false,
// i18next should fall back to the fallbackLng ("en")
expect(i18n.language).toBe("en");
// Test another unsupported region code
await i18n.changeLanguage("ja-JP");
// Even with nonExplicitSupportedLngs: false, i18next still falls back to base language
// if it exists in supportedLngs, but importantly, it won't make a 404 request first
expect(i18n.language).toBe("ja");
// Test that supported languages still work
await i18n.changeLanguage("ja");
expect(i18n.language).toBe("ja");
await i18n.changeLanguage("zh-CN");
expect(i18n.language).toBe("zh-CN");
} finally {
// Restore the original language
await i18n.changeLanguage(originalLanguage);
}
});
it("should have proper i18n configuration", () => {
// Test that the i18n instance has the expected configuration
expect(i18n.options.supportedLngs).toBeDefined();
// nonExplicitSupportedLngs should be false to prevent 404 errors
expect(i18n.options.nonExplicitSupportedLngs).toBe(false);
// fallbackLng can be a string or array, check if it includes "en"
const fallbackLng = i18n.options.fallbackLng;
if (Array.isArray(fallbackLng)) {
expect(fallbackLng).toContain("en");
} else {
expect(fallbackLng).toBe("en");
}
// Test that supported languages include both base and region-specific codes
const supportedLngs = i18n.options.supportedLngs as string[];
expect(supportedLngs).toContain("en");
expect(supportedLngs).toContain("zh-CN");
expect(supportedLngs).toContain("zh-TW");
expect(supportedLngs).toContain("ko-KR");
});
});
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "openhands-frontend",
"version": "0.53.0",
"version": "0.52.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openhands-frontend",
"version": "0.53.0",
"version": "0.52.1",
"dependencies": {
"@heroui/react": "^2.8.2",
"@heroui/use-infinite-scroll": "^2.2.10",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.53.0",
"version": "0.52.1",
"private": true,
"type": "module",
"engines": {
@@ -100,17 +100,6 @@ export function ConfigureModal({
}
}, [isOpen, existingWorkspace, isWorkspaceEditable]);
// Helper function to get platform-specific placeholder
const getWorkspacePlaceholder = () => {
if (platform === "jira") {
return I18nKey.PROJECT_MANAGEMENT$JIRA_WORKSPACE_NAME_PLACEHOLDER;
}
if (platform === "jira-dc") {
return I18nKey.PROJECT_MANAGEMENT$JIRA_DC_WORKSPACE_NAME_PLACEHOLDER;
}
return I18nKey.PROJECT_MANAGEMENT$LINEAR_WORKSPACE_NAME_PLACEHOLDER;
};
// Validation states
const [workspaceError, setWorkspaceError] = useState<string | null>(null);
const [webhookSecretError, setWebhookSecretError] = useState<string | null>(
@@ -279,11 +268,8 @@ export function ConfigureModal({
<BaseModalDescription>
{showConfigurationFields ? (
<Trans
i18nKey={
I18nKey.PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION_STAGE_2
}
i18nKey={I18nKey.PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION}
components={{
b: <b />,
a: (
<a
href="https://docs.all-hands.dev/usage/cloud/openhands-cloud"
@@ -294,41 +280,41 @@ export function ConfigureModal({
Check the document for more information
</a>
),
b: <b />,
}}
/>
) : (
<Trans
i18nKey={
I18nKey.PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION_STAGE_1
}
components={{
b: <b />,
a: (
<a
href="https://docs.all-hands.dev/usage/cloud/openhands-cloud"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 underline"
>
Check the document for more information
</a>
),
}}
/>
<p className="mt-4">
<Trans
i18nKey={
I18nKey.PROJECT_MANAGEMENT$IMPORTANT_WORKSPACE_INTEGRATION
}
components={{
b: <b />,
a: (
<a
href="https://docs.all-hands.dev/usage/cloud/openhands-cloud"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 underline"
>
Check the document for more information
</a>
),
}}
/>
</p>
)}
<p className="mt-4">
{t(I18nKey.PROJECT_MANAGEMENT$WORKSPACE_NAME_HINT, {
platform: platformName,
})}
</p>
</BaseModalDescription>
<div className="w-full flex flex-col gap-4 mt-1">
<div className="w-full flex flex-col gap-4 mt-4">
<div>
<div className="flex gap-2 items-end">
<div className="flex-1">
<SettingsInput
label={t(I18nKey.PROJECT_MANAGEMENT$WORKSPACE_NAME_LABEL)}
placeholder={t(getWorkspacePlaceholder())}
placeholder={t(
I18nKey.PROJECT_MANAGEMENT$WORKSPACE_NAME_PLACEHOLDER,
)}
value={workspace}
onChange={handleWorkspaceChange}
className="w-full"
@@ -432,7 +418,7 @@ export function ConfigureModal({
>
{(() => {
if (existingWorkspace && showConfigurationFields) {
return t(I18nKey.PROJECT_MANAGEMENT$UPDATE_BUTTON_LABEL);
return t(I18nKey.PROJECT_MANAGEMENT$EDIT_BUTTON_LABEL);
}
return t(I18nKey.PROJECT_MANAGEMENT$CONNECT_BUTTON_LABEL);
})()}
+4 -8
View File
@@ -749,15 +749,8 @@ export enum I18nKey {
PROJECT_MANAGEMENT$LINK_CONFIRMATION_TITLE = "PROJECT_MANAGEMENT$LINK_CONFIRMATION_TITLE",
PROJECT_MANAGEMENT$CONFIGURE_BUTTON_LABEL = "PROJECT_MANAGEMENT$CONFIGURE_BUTTON_LABEL",
PROJECT_MANAGEMENT$EDIT_BUTTON_LABEL = "PROJECT_MANAGEMENT$EDIT_BUTTON_LABEL",
PROJECT_MANAGEMENT$UPDATE_BUTTON_LABEL = "PROJECT_MANAGEMENT$UPDATE_BUTTON_LABEL",
PROJECT_MANAGEMENT$CONFIGURE_MODAL_TITLE = "PROJECT_MANAGEMENT$CONFIGURE_MODAL_TITLE",
PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION_STAGE_1 = "PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION_STAGE_1",
PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION_STAGE_2 = "PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION_STAGE_2",
PROJECT_MANAGEMENT$WORKSPACE_NAME_HINT = "PROJECT_MANAGEMENT$WORKSPACE_NAME_HINT",
PROJECT_MANAGEMENT$WORKSPACE_NAME_LABEL = "PROJECT_MANAGEMENT$WORKSPACE_NAME_LABEL",
PROJECT_MANAGEMENT$JIRA_WORKSPACE_NAME_PLACEHOLDER = "PROJECT_MANAGEMENT$JIRA_WORKSPACE_NAME_PLACEHOLDER",
PROJECT_MANAGEMENT$JIRA_DC_WORKSPACE_NAME_PLACEHOLDER = "PROJECT_MANAGEMENT$JIRA_DC_WORKSPACE_NAME_PLACEHOLDER",
PROJECT_MANAGEMENT$LINEAR_WORKSPACE_NAME_PLACEHOLDER = "PROJECT_MANAGEMENT$LINEAR_WORKSPACE_NAME_PLACEHOLDER",
PROJECT_MANAGEMENT$WORKSPACE_NAME_PLACEHOLDER = "PROJECT_MANAGEMENT$WORKSPACE_NAME_PLACEHOLDER",
PROJECT_MANAGEMENT$WEBHOOK_SECRET_LABEL = "PROJECT_MANAGEMENT$WEBHOOK_SECRET_LABEL",
PROJECT_MANAGEMENT$WEBHOOK_SECRET_PLACEHOLDER = "PROJECT_MANAGEMENT$WEBHOOK_SECRET_PLACEHOLDER",
PROJECT_MANAGEMENT$SERVICE_ACCOUNT_EMAIL_LABEL = "PROJECT_MANAGEMENT$SERVICE_ACCOUNT_EMAIL_LABEL",
@@ -766,6 +759,9 @@ export enum I18nKey {
PROJECT_MANAGEMENT$SERVICE_ACCOUNT_API_PLACEHOLDER = "PROJECT_MANAGEMENT$SERVICE_ACCOUNT_API_PLACEHOLDER",
PROJECT_MANAGEMENT$CONNECT_BUTTON_LABEL = "PROJECT_MANAGEMENT$CONNECT_BUTTON_LABEL",
PROJECT_MANAGEMENT$ACTIVE_TOGGLE_LABEL = "PROJECT_MANAGEMENT$ACTIVE_TOGGLE_LABEL",
PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION = "PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION",
PROJECT_MANAGEMENT$CONFIGURE_MODAL_TITLE = "PROJECT_MANAGEMENT$CONFIGURE_MODAL_TITLE",
PROJECT_MANAGEMENT$IMPORTANT_WORKSPACE_INTEGRATION = "PROJECT_MANAGEMENT$IMPORTANT_WORKSPACE_INTEGRATION",
PROJECT_MANAGEMENT$WORKSPACE_NAME_VALIDATION_ERROR = "PROJECT_MANAGEMENT$WORKSPACE_NAME_VALIDATION_ERROR",
PROJECT_MANAGEMENT$WEBHOOK_SECRET_NAME_VALIDATION_ERROR = "PROJECT_MANAGEMENT$WEBHOOK_SECRET_NAME_VALIDATION_ERROR",
PROJECT_MANAGEMENT$SVC_ACC_EMAIL_VALIDATION_ERROR = "PROJECT_MANAGEMENT$SVC_ACC_EMAIL_VALIDATION_ERROR",
+1 -9
View File
@@ -27,15 +27,7 @@ i18n
.init({
fallbackLng: "en",
debug: import.meta.env.NODE_ENV === "development",
// Define supported languages explicitly to prevent 404 errors
// According to i18next documentation, this is the recommended way to prevent
// 404 requests for unsupported language codes like 'en-US@posix'
supportedLngs: AvailableLanguages.map((lang) => lang.value),
// Do NOT set nonExplicitSupportedLngs: true as it causes 404 errors
// for region-specific codes not in supportedLngs (per i18next developer)
nonExplicitSupportedLngs: false,
load: "currentOnly",
});
export default i18n;
+63 -127
View File
@@ -11983,86 +11983,6 @@
"de": "Bearbeiten",
"uk": "Редагувати"
},
"PROJECT_MANAGEMENT$UPDATE_BUTTON_LABEL": {
"en": "Update",
"ja": "更新",
"zh-CN": "更新",
"zh-TW": "更新",
"ko-KR": "업데이트",
"no": "Oppdater",
"it": "Aggiorna",
"pt": "Atualizar",
"es": "Actualizar",
"ar": "تحديث",
"fr": "Mettre à jour",
"tr": "Güncelle",
"de": "Aktualisieren",
"uk": "Оновити"
},
"PROJECT_MANAGEMENT$CONFIGURE_MODAL_TITLE": {
"en": "Configure {{platform}} Integration",
"ja": "{{platform}}統合を設定",
"zh-CN": "配置{{platform}}集成",
"zh-TW": "配置{{platform}}集成",
"ko-KR": "{{platform}} 통합 구성",
"no": "Konfigurer {{platform}}-integrasjon",
"it": "Configura integrazione {{platform}}",
"pt": "Configurar integração {{platform}}",
"es": "Configurar integración de {{platform}}",
"ar": "تكوين تكامل {{platform}}",
"fr": "Configurer l'intégration {{platform}}",
"tr": "{{platform}} Entegrasyonunu Yapılandır",
"de": "{{platform}}-Integration konfigurieren",
"uk": "Налаштувати інтеграцію {{platform}}"
},
"PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION_STAGE_1": {
"en": "<b>Important: </b>Make sure the workspace integration for your target workspace is already configured. Check the <a>documentation</a> for more information.",
"ja": "<b>重要: </b>対象のワークスペースのワークスペース統合がすでに設定されていることを確認してください。詳しくは<a>ドキュメント</a>をご覧ください。",
"zh-CN": "<b>重要提示:</b>请确保目标工作区的工作区集成已配置完毕。查看<a>文档</a>了解更多信息。",
"zh-TW": "<b>重要提示:</b>請確保目標工作區的工作區整合已設定完成。查看<a>文件</a>以了解更多資訊。",
"ko-KR": "<b>중요:</b>대상 작업 공간에 대한 작업 공간 통합이 이미 구성되어 있는지 확인하세요. 자세한 내용은 <a>설명서</a>를 참조하세요.",
"no": "<b>Viktig:</b>Sørg for at arbeidsområdeintegrasjonen for målarbeidsområdet ditt allerede er konfigurert. Se <a>dokumentasjonen</a> for mer informasjon.",
"it": "<b>Importante: </b>Assicurati che l'integrazione dell'area di lavoro per l'area di lavoro di destinazione sia già configurata. Consulta la <a>documentazione</a> per ulteriori informazioni.",
"pt": "<b>Importante:</b>Certifique-se de que a integração do workspace de destino já esteja configurada. Consulte a <a>documentação</a> para obter mais informações.",
"es": "Importante: Asegúrate de que la integración del espacio de trabajo de destino ya esté configurada. Consulta la documentación para obtener más información.",
"ar": "<b>هام: </b>تأكد من إعداد تكامل مساحة العمل لمساحة العمل المستهدفة. <a>راجع المستند لمزيد من المعلومات</a>.",
"fr": "<b>Important :</b>Assurez-vous que l'intégration de l'espace de travail cible est déjà configurée. Consultez la <a>documentation</a> pour plus d'informations.",
"tr": "<b>Önemli: </b>Hedef çalışma alanınız için çalışma alanı entegrasyonunun zaten yapılandırılmış olduğundan emin olun. Daha fazla bilgi için <a>belgelere</a> bakın.",
"de": "<b>Wichtig:</b>Stellen Sie sicher, dass die Arbeitsbereichsintegration für Ihren Zielarbeitsbereich bereits konfiguriert ist. Weitere Informationen finden Sie in der <a>Dokumentation</a>.",
"uk": "<b>Важливо: </b>Переконайтеся, що інтеграцію робочого простору для вашого цільового робочого простору вже налаштовано. Перегляньте <a>документацію</a> для отримання додаткової інформації."
},
"PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION_STAGE_2": {
"en": "<b>Important:</b> Check the <a>documentation</a> for more information about configuring the workspace integration or updating an existing integration.",
"ja": "<b>重要:</b> ワークスペース統合の設定や既存の統合の更新について詳しくは<a>ドキュメント</a>をご確認ください。",
"zh-CN": "<b>重要提示:</b>有关配置工作区集成或更新现有集成的更多信息,请查看<a>文档</a>。",
"zh-TW": "<b>重要提示:</b>有關配置工作區整合或更新現有整合的更多資訊,請查看<a>文件</a>。",
"ko-KR": "<b>중요:</b> 작업공간 통합 구성이나 기존 통합 업데이트에 대한 자세한 내용은 <a>설명서</a>를 확인하세요.",
"no": "<b>Viktig:</b> Se <a>dokumentasjonen</a> for mer informasjon om konfigurering av arbeidsområdeintegrasjon eller oppdatering av eksisterende integrasjon.",
"it": "<b>Importante:</b> Consulta la <a>documentazione</a> per ulteriori informazioni sulla configurazione dell'integrazione dell'area di lavoro o sull'aggiornamento di un'integrazione esistente.",
"pt": "<b>Importante:</b> Consulte a <a>documentação</a> para obter mais informações sobre como configurar a integração do workspace ou atualizar uma integração existente.",
"es": "<b>Importante:</b> Consulte la <a>documentación</a> para obtener más información sobre la configuración de la integración del espacio de trabajo o la actualización de una integración existente.",
"ar": "<b>هام:</b> راجع <a>الوثائق</a> لمزيد من المعلومات حول تكوين تكامل مساحة العمل أو تحديث تكامل موجود.",
"fr": "<b>Important :</b> Consultez la <a>documentation</a> pour plus d'informations sur la configuration de l'intégration de l'espace de travail ou la mise à jour d'une intégration existante.",
"tr": "<b>Önemli:</b> Çalışma alanı entegrasyonunu yapılandırma veya mevcut bir entegrasyonu güncelleme hakkında daha fazla bilgi için <a>belgelere</a> bakın.",
"de": "<b>Wichtig:</b> Weitere Informationen zur Konfiguration der Arbeitsbereichsintegration oder zur Aktualisierung einer bestehenden Integration finden Sie in der <a>Dokumentation</a>.",
"uk": "<b>Важливо:</b> Перегляньте <a>документацію</a> для отримання додаткової інформації про налаштування інтеграції робочого простору або оновлення існуючої інтеграції."
},
"PROJECT_MANAGEMENT$WORKSPACE_NAME_HINT": {
"en": "Workspace name can be found in the browser URL when you're accessing a resource (eg: issue) in {{platform}}.",
"ja": "ワークスペース名は、{{platform}}のリソース(例:イシュー)にアクセスする際のブラウザURLで確認できます。",
"zh-CN": "工作区名称可以在访问{{platform}}资源(如:问题)时的浏览器URL中找到。",
"zh-TW": "工作區名稱可以在存取{{platform}}資源(如:問題)時的瀏覽器URL中找到。",
"ko-KR": "작업공간 이름은 {{platform}}의 리소스(예: 이슈)에 액세스할 때 브라우저 URL에서 찾을 수 있습니다.",
"no": "Arbeidsområdenavn kan finnes i nettleser-URL-en når du får tilgang til en ressurs (f.eks: sak) i {{platform}}.",
"it": "Il nome dell'area di lavoro può essere trovato nell'URL del browser quando stai accedendo a una risorsa (es: issue) in {{platform}}.",
"pt": "O nome do workspace pode ser encontrado na URL do navegador quando você está acessando um recurso (ex: issue) em {{platform}}.",
"es": "El nombre del espacio de trabajo se puede encontrar en la URL del navegador cuando accedes a un recurso (ej: issue) en {{platform}}.",
"ar": "يمكن العثور على اسم مساحة العمل في عنوان URL للمتصفح عند الوصول إلى مورد (مثل: مشكلة) في {{platform}}.",
"fr": "Le nom de l'espace de travail peut être trouvé dans l'URL du navigateur lorsque vous accédez à une ressource (ex : issue) dans {{platform}}.",
"tr": "Çalışma alanı adı, {{platform}}'da bir kaynağa (örn: sorun) erişirken tarayıcı URL'sinde bulunabilir.",
"de": "Der Arbeitsbereichsname ist in der Browser-URL zu finden, wenn Sie auf eine Ressource (z.B.: Issue) in {{platform}} zugreifen.",
"uk": "Назву робочого простору можна знайти в URL браузера під час доступу до ресурсу (наприклад: проблема) в {{platform}}."
},
"PROJECT_MANAGEMENT$WORKSPACE_NAME_LABEL": {
"en": "Workspace Name",
"ja": "ワークスペース名",
@@ -12079,53 +11999,21 @@
"de": "Arbeitsbereichsname",
"uk": "Назва робочої області"
},
"PROJECT_MANAGEMENT$JIRA_WORKSPACE_NAME_PLACEHOLDER": {
"en": "yourcompany.atlassian.net",
"ja": "yourcompany.atlassian.net",
"zh-CN": "yourcompany.atlassian.net",
"zh-TW": "yourcompany.atlassian.net",
"ko-KR": "yourcompany.atlassian.net",
"no": "yourcompany.atlassian.net",
"it": "yourcompany.atlassian.net",
"pt": "yourcompany.atlassian.net",
"es": "yourcompany.atlassian.net",
"ar": "yourcompany.atlassian.net",
"fr": "yourcompany.atlassian.net",
"tr": "yourcompany.atlassian.net",
"de": "yourcompany.atlassian.net",
"uk": "yourcompany.atlassian.net"
},
"PROJECT_MANAGEMENT$JIRA_DC_WORKSPACE_NAME_PLACEHOLDER": {
"en": "jira.yourcompany.com",
"ja": "jira.yourcompany.com",
"zh-CN": "jira.yourcompany.com",
"zh-TW": "jira.yourcompany.com",
"ko-KR": "jira.yourcompany.com",
"no": "jira.yourcompany.com",
"it": "jira.yourcompany.com",
"pt": "jira.yourcompany.com",
"es": "jira.yourcompany.com",
"ar": "jira.yourcompany.com",
"fr": "jira.yourcompany.com",
"tr": "jira.yourcompany.com",
"de": "jira.yourcompany.com",
"uk": "jira.yourcompany.com"
},
"PROJECT_MANAGEMENT$LINEAR_WORKSPACE_NAME_PLACEHOLDER": {
"en": "yourcompany",
"ja": "yourcompany",
"zh-CN": "yourcompany",
"zh-TW": "yourcompany",
"ko-KR": "yourcompany",
"no": "yourcompany",
"it": "yourcompany",
"pt": "yourcompany",
"es": "yourcompany",
"ar": "yourcompany",
"fr": "yourcompany",
"tr": "yourcompany",
"de": "yourcompany",
"uk": "yourcompany"
"PROJECT_MANAGEMENT$WORKSPACE_NAME_PLACEHOLDER": {
"en": "myworkspace",
"ja": "私のワークスペース",
"zh-CN": "我的工作空间",
"zh-TW": "我的工作區",
"ko-KR": "내워크스페이스",
"no": "mittarbeidsområde",
"it": "mioworkspace",
"pt": "meuworkspace",
"es": "miespaciodetrabajo",
"ar": "مساحةعملي",
"fr": "monworkspace",
"tr": "benimworkspace",
"de": "meinarbeitsbereich",
"uk": "моя-робоча-область"
},
"PROJECT_MANAGEMENT$WEBHOOK_SECRET_LABEL": {
"en": "Webhook Secret",
@@ -12255,6 +12143,54 @@
"de": "Aktiv",
"uk": "Активний"
},
"PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION": {
"en": "<b>Important:</b> Check the <a>documentation</a> for more information about configuring the workspace integration or updating an existing integration.",
"ja": "<b>重要:</b> ワークスペース統合の設定や既存の統合の更新について詳しくは<a>ドキュメント</a>をご確認ください。",
"zh-CN": "<b>重要提示:</b>有关配置工作区集成或更新现有集成的更多信息,请查看<a>文档</a>。",
"zh-TW": "<b>重要提示:</b>有關配置工作區整合或更新現有整合的更多資訊,請查看<a>文件</a>。",
"ko-KR": "<b>중요:</b> 작업공간 통합 구성이나 기존 통합 업데이트에 대한 자세한 내용은 <a>설명서</a>를 확인하세요.",
"no": "<b>Viktig:</b> Se <a>dokumentasjonen</a> for mer informasjon om konfigurering av arbeidsområdeintegrasjon eller oppdatering av eksisterende integrasjon.",
"it": "<b>Importante:</b> Consulta la <a>documentazione</a> per ulteriori informazioni sulla configurazione dell'integrazione dell'area di lavoro o sull'aggiornamento di un'integrazione esistente.",
"pt": "<b>Importante:</b> Consulte a <a>documentação</a> para obter mais informações sobre como configurar a integração do workspace ou atualizar uma integração existente.",
"es": "<b>Importante:</b> Consulte la <a>documentación</a> para obtener más información sobre la configuración de la integración del espacio de trabajo o la actualización de una integración existente.",
"ar": "<b>هام:</b> راجع <a>الوثائق</a> لمزيد من المعلومات حول تكوين تكامل مساحة العمل أو تحديث تكامل موجود.",
"fr": "<b>Important :</b> Consultez la <a>documentation</a> pour plus d'informations sur la configuration de l'intégration de l'espace de travail ou la mise à jour d'une intégration existante.",
"tr": "<b>Önemli:</b> Çalışma alanı entegrasyonunu yapılandırma veya mevcut bir entegrasyonu güncelleme hakkında daha fazla bilgi için <a>belgelere</a> bakın.",
"de": "<b>Wichtig:</b> Weitere Informationen zur Konfiguration der Arbeitsbereichsintegration oder zur Aktualisierung einer bestehenden Integration finden Sie in der <a>Dokumentation</a>.",
"uk": "<b>Важливо:</b> Перегляньте <a>документацію</a> для отримання додаткової інформації про налаштування інтеграції робочого простору або оновлення існуючої інтеграції."
},
"PROJECT_MANAGEMENT$CONFIGURE_MODAL_TITLE": {
"en": "Configure {{platform}} Integration",
"ja": "{{platform}}統合を設定",
"zh-CN": "配置{{platform}}集成",
"zh-TW": "配置{{platform}}集成",
"ko-KR": "{{platform}} 통합 구성",
"no": "Konfigurer {{platform}}-integrasjon",
"it": "Configura integrazione {{platform}}",
"pt": "Configurar integração {{platform}}",
"es": "Configurar integración de {{platform}}",
"ar": "تكوين تكامل {{platform}}",
"fr": "Configurer l'intégration {{platform}}",
"tr": "{{platform}} Entegrasyonunu Yapılandır",
"de": "{{platform}}-Integration konfigurieren",
"uk": "Налаштувати інтеграцію {{platform}}"
},
"PROJECT_MANAGEMENT$IMPORTANT_WORKSPACE_INTEGRATION": {
"en": "<b>Important: </b>Make sure the workspace integration for your target workspace is already configured. Check the <a>documentation</a> for more information.",
"ja": "<b>重要: </b>対象のワークスペースのワークスペース統合がすでに設定されていることを確認してください。詳しくは<a>ドキュメント</a>をご覧ください。",
"zh-CN": "<b>重要提示:</b>请确保目标工作区的工作区集成已配置完毕。查看<a>文档</a>了解更多信息。",
"zh-TW": "<b>重要提示:</b>請確保目標工作區的工作區整合已設定完成。查看<a>文件</a>以了解更多資訊。",
"ko-KR": "<b>중요:</b>대상 작업 공간에 대한 작업 공간 통합이 이미 구성되어 있는지 확인하세요. 자세한 내용은 <a>설명서</a>를 참조하세요.",
"no": "<b>Viktig:</b>Sørg for at arbeidsområdeintegrasjonen for målarbeidsområdet ditt allerede er konfigurert. Se <a>dokumentasjonen</a> for mer informasjon.",
"it": "<b>Importante: </b>Assicurati che l'integrazione dell'area di lavoro per l'area di lavoro di destinazione sia già configurata. Consulta la <a>documentazione</a> per ulteriori informazioni.",
"pt": "<b>Importante:</b>Certifique-se de que a integração do workspace de destino já esteja configurada. Consulte a <a>documentação</a> para obter mais informações.",
"es": "Importante: Asegúrate de que la integración del espacio de trabajo de destino ya esté configurada. Consulta la documentación para obtener más información.",
"ar": "<b>هام: </b>تأكد من إعداد تكامل مساحة العمل لمساحة العمل المستهدفة. <a>راجع المستند لمزيد من المعلومات</a>.",
"fr": "<b>Important :</b>Assurez-vous que l'intégration de l'espace de travail cible est déjà configurée. Consultez la <a>documentation</a> pour plus d'informations.",
"tr": "<b>Önemli: </b>Hedef çalışma alanınız için çalışma alanı entegrasyonunun zaten yapılandırılmış olduğundan emin olun. Daha fazla bilgi için <a>belgelere</a> bakın.",
"de": "<b>Wichtig:</b>Stellen Sie sicher, dass die Arbeitsbereichsintegration für Ihren Zielarbeitsbereich bereits konfiguriert ist. Weitere Informationen finden Sie in der <a>Dokumentation</a>.",
"uk": "<b>Важливо: </b>Переконайтеся, що інтеграцію робочого простору для вашого цільового робочого простору вже налаштовано. Перегляньте <a>документацію</a> для отримання додаткової інформації."
},
"PROJECT_MANAGEMENT$WORKSPACE_NAME_VALIDATION_ERROR": {
"en": "Workspace name can only contain letters, numbers, hyphens, and underscores",
"ja": "ワークスペース名は文字、数字、ハイフン、アンダースコアのみ使用できます",
+106 -5
View File
@@ -3,6 +3,7 @@ import os
import time
import warnings
from functools import partial
from threading import RLock
from typing import Any, Callable
import httpx
@@ -142,6 +143,7 @@ class LLM(RetryMixin, DebugMixin):
metrics: The metrics to use.
"""
self._tried_model_info = False
self._lock = RLock()
self.metrics: Metrics = (
metrics if metrics is not None else Metrics(model_name=config.model)
)
@@ -157,11 +159,22 @@ class LLM(RetryMixin, DebugMixin):
)
os.makedirs(self.config.log_completions_folder, exist_ok=True)
# call init_model_info to initialize config.max_output_tokens
# which is used in partial function
# Initialize core internals in a single pass
self._initialize_core()
def _initialize_core(self) -> None:
"""Initialize or re-initialize all components derived from config.
This centralizes initialization to avoid duplication between __init__ and reinit().
"""
# call init_model_info to initialize config.max_output_tokens used in partial
with warnings.catch_warnings():
warnings.simplefilter('ignore')
self.init_model_info()
# Recompute function-calling capability regardless of model_info cache
self._compute_function_calling_active()
if self.vision_is_active():
logger.debug('LLM: model has vision enabled')
if self.is_caching_prompt_active():
@@ -175,6 +188,17 @@ class LLM(RetryMixin, DebugMixin):
else:
self.tokenizer = None
# Initialize the completion function
self._build_completion_function()
# Build the completion wrapper with retry logic
self._rebuild_completion_wrapper()
def _build_completion_function(self) -> None:
"""Build the completion function based on current configuration.
This method creates the partial function that will be used for LLM completions.
It can be called multiple times to rebuild the function when configuration changes.
"""
# set up the completion function
kwargs: dict[str, Any] = {
'temperature': self.config.temperature,
@@ -251,6 +275,78 @@ class LLM(RetryMixin, DebugMixin):
self._completion_unwrapped = self._completion
def reinit(self, new_config: LLMConfig) -> None:
"""Reinitialize the LLM with a new configuration.
This is a threadsafe operation that updates configuration and rebuilds
the completion pipeline (completion function + retry wrapper). It also
refreshes model info and tokenizer as needed.
"""
with self._lock:
# Reset capability/cost flags so the new config gets a clean slate
self.cost_metric_supported = True
old_model = self.config.model
old_tokenizer = self.config.custom_tokenizer
# Update the configuration (deep copy to avoid external mutation)
self.config = copy.deepcopy(new_config)
# Update metrics model name if model changed and refresh model info
if old_model != new_config.model:
self.metrics.model_name = new_config.model
logger.debug(
f'LLM model changed from {old_model} to {new_config.model}'
)
# Reset model info to force re-initialization
self._tried_model_info = False
self.model_info = None
with warnings.catch_warnings():
warnings.simplefilter('ignore')
self.init_model_info()
# Log new capabilities
if self.vision_is_active():
logger.debug('LLM: model has vision enabled')
if self.is_caching_prompt_active():
logger.debug('LLM: caching prompt enabled')
if self.is_function_calling_active():
logger.debug('LLM: model supports function calling')
# Update tokenizer if custom_tokenizer changed
if old_tokenizer != new_config.custom_tokenizer:
if new_config.custom_tokenizer is not None:
self.tokenizer = create_pretrained_tokenizer(
new_config.custom_tokenizer
)
logger.debug(
f'LLM tokenizer updated to {new_config.custom_tokenizer}'
)
else:
self.tokenizer = None
logger.debug('LLM tokenizer reset to default')
# Handle log completions folder creation if needed
if new_config.log_completions:
if new_config.log_completions_folder is None:
raise RuntimeError(
'log_completions_folder is required when log_completions is enabled'
)
os.makedirs(new_config.log_completions_folder, exist_ok=True)
# Re-initialize core internals (model info, tokenizer, completion & wrapper)
self._initialize_core()
logger.debug('LLM reinitialized successfully')
# Backward-compat: keep update_config as an alias
def update_config(self, new_config: LLMConfig) -> None:
self.reinit(new_config)
def _rebuild_completion_wrapper(self) -> None:
"""Rebuild the completion wrapper with retry decorator.
This method recreates the wrapper function that includes retry logic
and other processing around the base completion function.
"""
@self.retry_decorator(
num_retries=self.config.num_retries,
retry_exceptions=LLM_RETRY_EXCEPTIONS,
@@ -553,14 +649,19 @@ class LLM(RetryMixin, DebugMixin):
self.config.max_output_tokens = self.model_info['max_tokens']
# Initialize function calling capability
# Check if model name is in our supported list
self._compute_function_calling_active()
def _compute_function_calling_active(self) -> None:
"""Compute and cache whether function calling is active for current config.
Respects user override via native_tool_calling. Otherwise bases decision on
supported model names.
"""
model_name_supported = (
self.config.model in FUNCTION_CALLING_SUPPORTED_MODELS
or self.config.model.split('/')[-1] in FUNCTION_CALLING_SUPPORTED_MODELS
or any(m in self.config.model for m in FUNCTION_CALLING_SUPPORTED_MODELS)
)
# Handle native_tool_calling user-defined configuration
if self.config.native_tool_calling is None:
self._function_calling_active = model_name_supported
else:
+1 -1
View File
@@ -40,7 +40,7 @@ Two configuration options are required to use the Kubernetes runtime:
2. **Runtime Container Image**: Specify the container image to use for the runtime environment
```toml
[sandbox]
runtime_container_image = "docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik"
runtime_container_image = "docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik"
```
#### Additional Kubernetes Options
+1 -1
View File
@@ -6,7 +6,7 @@ requires = [
[tool.poetry]
name = "openhands-ai"
version = "0.53.0"
version = "0.52.1"
description = "OpenHands: Code Less, Make More"
authors = [ "OpenHands" ]
license = "MIT"
+352 -1
View File
@@ -1104,7 +1104,219 @@ def test_azure_model_default_max_tokens():
assert llm.config.max_output_tokens is None # Default value
# Gemini Performance Optimization Tests
def test_llm_update_config_basic(default_config):
"""Test basic LLM configuration update functionality."""
llm = LLM(default_config)
# Verify initial state
assert llm.config.model == 'gpt-4o'
assert llm.config.temperature == 0.0
assert llm.metrics.model_name == 'gpt-4o'
# Create new config with different settings
new_config = LLMConfig(
model='claude-3-5-sonnet-20241022',
api_key='new_test_key',
temperature=0.7,
max_output_tokens=2000,
top_p=0.9,
)
# Update the configuration
llm.update_config(new_config)
# Verify the configuration was updated
assert llm.config.model == 'claude-3-5-sonnet-20241022'
assert llm.config.api_key.get_secret_value() == 'new_test_key'
assert llm.config.temperature == 0.7
assert llm.config.max_output_tokens == 2000
assert llm.config.top_p == 0.9
assert llm.metrics.model_name == 'claude-3-5-sonnet-20241022'
def test_llm_update_config_model_change_resets_model_info(default_config):
"""Test that changing model resets model info for re-initialization."""
llm = LLM(default_config)
# Set some model info to verify it gets reset
llm.model_info = {'test': 'info'}
llm._tried_model_info = True
# Create new config with different model
new_config = copy.deepcopy(default_config)
new_config.model = 'claude-3-5-sonnet-20241022'
# Update the configuration
llm.update_config(new_config)
# Verify model info was reset and metrics updated
assert llm.metrics.model_name == 'claude-3-5-sonnet-20241022'
# _tried_model_info should be reset to False to force re-initialization
# (it will be set back to True after init_model_info() is called)
def test_llm_update_config_same_model_preserves_model_info(default_config):
"""Test that keeping the same model preserves model info."""
llm = LLM(default_config)
# Create new config with same model but different other settings
new_config = copy.deepcopy(default_config)
new_config.temperature = 0.5
new_config.max_output_tokens = 1500
# Update the configuration
llm.update_config(new_config)
# Verify model info was preserved but other settings changed
assert llm.config.temperature == 0.5
assert llm.config.max_output_tokens == 1500
assert llm.metrics.model_name == 'gpt-4o' # Same model
def test_llm_update_config_custom_tokenizer_update(default_config):
"""Test updating custom tokenizer configuration."""
llm = LLM(default_config)
# Initially no custom tokenizer
assert llm.config.custom_tokenizer is None
assert llm.tokenizer is None
# Update config with custom tokenizer
new_config = copy.deepcopy(default_config)
new_config.custom_tokenizer = 'gpt2'
llm.update_config(new_config)
# Verify tokenizer was updated
assert llm.config.custom_tokenizer == 'gpt2'
assert llm.tokenizer is not None
# Update back to no custom tokenizer
new_config.custom_tokenizer = None
llm.update_config(new_config)
# Verify tokenizer was reset
assert llm.config.custom_tokenizer is None
assert llm.tokenizer is None
def test_llm_update_config_log_completions_folder_creation(default_config):
"""Test that log completions folder is created when needed."""
with tempfile.TemporaryDirectory() as temp_dir:
log_folder = Path(temp_dir) / 'test_completions'
llm = LLM(default_config)
# Update config to enable log completions
new_config = copy.deepcopy(default_config)
new_config.log_completions = True
new_config.log_completions_folder = str(log_folder)
# Folder shouldn't exist yet
assert not log_folder.exists()
# Update configuration
llm.update_config(new_config)
# Verify folder was created
assert log_folder.exists()
assert log_folder.is_dir()
def test_llm_update_config_log_completions_folder_required():
"""Test that log_completions_folder is required when log_completions is True."""
config = LLMConfig(model='gpt-4o', api_key='test_key')
llm = LLM(config)
# Create config with log_completions=True but no folder
new_config = copy.deepcopy(config)
new_config.log_completions = True
new_config.log_completions_folder = None
# Should raise RuntimeError
with pytest.raises(RuntimeError, match='log_completions_folder is required'):
llm.update_config(new_config)
def test_llm_update_config_completion_function_rebuilt(default_config):
"""Test that completion function is rebuilt after config update."""
llm = LLM(default_config)
# Store reference to original completion function
# Update configuration
new_config = copy.deepcopy(default_config)
new_config.temperature = 0.8
new_config.max_output_tokens = 1500
llm.update_config(new_config)
# Verify completion functions exist (they should be rebuilt)
assert llm._completion is not None
assert llm._completion_unwrapped is not None
# The functions should be different objects since they were rebuilt
# (though this is implementation detail, the important thing is they work)
assert callable(llm._completion)
assert callable(llm._completion_unwrapped)
def test_llm_update_config_preserves_metrics_and_retry_listener(default_config):
"""Test that metrics and retry listener are preserved during config update."""
# Create custom metrics and retry listener
custom_metrics = Metrics(model_name='initial_model')
retry_listener = MagicMock()
llm = LLM(default_config, metrics=custom_metrics, retry_listener=retry_listener)
# Verify initial state
assert llm.metrics is custom_metrics
assert llm.retry_listener is retry_listener
# Update configuration
new_config = copy.deepcopy(default_config)
new_config.model = 'claude-3-5-sonnet-20241022'
llm.update_config(new_config)
# Verify metrics and retry listener are preserved
assert llm.metrics is custom_metrics
assert llm.retry_listener is retry_listener
# But metrics model name should be updated
assert llm.metrics.model_name == 'claude-3-5-sonnet-20241022'
def test_llm_update_config_deep_copy_independence():
"""Test that config update uses deep copy and doesn't affect original config."""
original_config = LLMConfig(
model='gpt-4o',
api_key='test_key',
temperature=0.0,
)
llm = LLM(original_config)
# Create new config
new_config = LLMConfig(
model='claude-3-5-sonnet-20241022',
api_key='new_key',
temperature=0.7,
)
# Update LLM config
llm.update_config(new_config)
# Modify the new_config after update
new_config.temperature = 0.9
new_config.model = 'different-model'
# LLM config should not be affected by external changes
assert llm.config.temperature == 0.7
assert llm.config.model == 'claude-3-5-sonnet-20241022'
# Original config should also be unchanged
assert original_config.temperature == 0.0
assert original_config.model == 'gpt-4o'
def test_gemini_model_keeps_none_reasoning_effort():
@@ -1302,3 +1514,142 @@ def test_gemini_performance_optimization_end_to_end(mock_completion):
# Verify temperature and top_p were removed for reasoning models
assert 'temperature' not in call_kwargs
assert 'top_p' not in call_kwargs
def test_llm_reinit_basic(default_config):
"""Reinit should update config and metrics like update_config."""
llm = LLM(default_config)
assert llm.metrics.model_name == 'gpt-4o'
new_config = LLMConfig(
model='claude-3-5-sonnet-20241022',
api_key='new_test_key',
temperature=0.7,
max_output_tokens=1234,
)
llm.reinit(new_config)
assert llm.config.model == 'claude-3-5-sonnet-20241022'
assert llm.config.api_key.get_secret_value() == 'new_test_key'
assert llm.config.temperature == 0.7
assert llm.config.max_output_tokens == 1234
assert llm.metrics.model_name == 'claude-3-5-sonnet-20241022'
assert callable(llm._completion)
assert callable(llm._completion_unwrapped)
def test_llm_reinit_recomputes_function_calling_capability():
"""Reinit should recompute function-calling capability even if model doesn't change."""
base = LLMConfig(model='gpt-3.5-turbo', api_key='key')
llm = LLM(base)
# gpt-3.5-turbo is not in FUNCTION_CALLING_SUPPORTED_MODELS
assert llm.is_function_calling_active() is False
# Turn on native tool calling; same model
cfg_on = copy.deepcopy(base)
cfg_on.native_tool_calling = True
llm.reinit(cfg_on)
assert llm.is_function_calling_active() is True
# Turn off explicitly
cfg_off = copy.deepcopy(base)
cfg_off.native_tool_calling = False
llm.reinit(cfg_off)
assert llm.is_function_calling_active() is False
def test_llm_reinit_resets_cost_flag(default_config):
"""Reinit should reset cost_metric_supported so a new model can report cost."""
llm = LLM(default_config)
llm.cost_metric_supported = False
# Same model, but reinit should reset the flag to True
llm.reinit(copy.deepcopy(default_config))
assert llm.cost_metric_supported is True
def test_llm_reinit_thread_safety_with_inflight_completion(default_config):
"""Concurrent reinit during an in-flight completion should not raise,
and subsequent completions should use the new config.
"""
import threading
import time
from unittest.mock import patch
llm = LLM(default_config)
calls = []
def fake_completion(*args, **kwargs):
# Simulate provider latency and record the model used
calls.append(kwargs.get('model'))
time.sleep(0.2)
return {
'id': 'test-1',
'choices': [{'message': {'content': 'ok'}}],
'usage': {'prompt_tokens': 1, 'completion_tokens': 1},
}
with patch('openhands.llm.llm.litellm_completion') as mock_completion:
mock_completion.side_effect = fake_completion
# Start an in-flight completion with the initial model
t = threading.Thread(
target=llm.completion, args=([{'role': 'user', 'content': 'hi'}],)
)
t.start()
# Reinit while the completion is in-flight
time.sleep(0.05)
new_cfg = copy.deepcopy(default_config)
new_cfg.model = 'claude-3-5-sonnet-20241022'
llm.reinit(new_cfg)
t.join()
# Subsequent completion should use the new config
llm.completion(messages=[{'role': 'user', 'content': 'again'}])
# Ensure the latest call used the new model and metrics reflect it
assert len(calls) >= 1
assert calls[-1] == 'claude-3-5-sonnet-20241022'
assert llm.metrics.model_name == 'claude-3-5-sonnet-20241022'
@patch('openhands.llm.llm.litellm_completion')
def test_llm_reinit_provider_mappings(mock_completion, default_config):
"""Reinit should apply provider-specific mappings (openhands proxy, azure max_tokens)."""
# Return a minimal, valid response
mock_completion.return_value = {
'id': 'call',
'choices': [{'message': {'content': 'ok'}}],
'usage': {'prompt_tokens': 1, 'completion_tokens': 1},
}
llm = LLM(default_config)
# 1) OpenHands provider rewrite to litellm_proxy
cfg_proxy = copy.deepcopy(default_config)
cfg_proxy.model = 'openhands/qwen3-coder'
llm.reinit(cfg_proxy)
llm.completion(messages=[{'role': 'user', 'content': 'x'}])
model_arg = mock_completion.call_args.kwargs.get('model')
base_url_arg = mock_completion.call_args.kwargs.get('base_url')
assert model_arg == 'litellm_proxy/qwen3-coder'
assert base_url_arg == 'https://llm-proxy.app.all-hands.dev/'
# 2) Azure mapping: max_completion_tokens -> max_tokens
mock_completion.reset_mock()
cfg_azure = copy.deepcopy(default_config)
cfg_azure.model = 'azure/gpt-4o'
cfg_azure.max_output_tokens = 777
cfg_azure.api_version = '2024-06-01'
llm.reinit(cfg_azure)
llm.completion(messages=[{'role': 'user', 'content': 'y'}])
kwargs = mock_completion.call_args.kwargs
assert kwargs.get('model') == 'azure/gpt-4o'
assert 'max_tokens' in kwargs and kwargs['max_tokens'] == 777
assert 'max_completion_tokens' not in kwargs