Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8d856cfa5 | |||
| db5c0c687c | |||
| 82d5b0388b | |||
| bf192688a5 | |||
| 477d9b17c0 | |||
| 6756427217 | |||
| 1b74920509 | |||
| c933b88f36 | |||
| e0e9d3d07c | |||
| 00f9ff08c7 | |||
| f37f8fb723 | |||
| 8628da0037 |
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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` 来将对话历史迁移到新位置。
|
||||
|
||||
@@ -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` を実行してください。
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
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">
|
||||

|
||||

|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Workspace Configure flow">
|
||||

|
||||

|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Edit view as a user">
|
||||

|
||||

|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Edit view as the workspace creator">
|
||||

|
||||

|
||||
</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">
|
||||

|
||||

|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Workspace Configure flow">
|
||||

|
||||

|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Edit view as a user">
|
||||

|
||||

|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Edit view as the workspace creator">
|
||||

|
||||

|
||||
</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">
|
||||

|
||||

|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Workspace Configure flow">
|
||||

|
||||

|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Edit view as a user">
|
||||

|
||||

|
||||
</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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,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);
|
||||
})()}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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": "ワークスペース名は文字、数字、ハイフン、アンダースコアのみ使用できます",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||