mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-05 23:55:47 +00:00
Compare commits
20 Commits
nikg/std-e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c10ffbb464 | ||
|
|
091f41fd1f | ||
|
|
45d77be4eb | ||
|
|
413fa85134 | ||
|
|
108cde4f55 | ||
|
|
f88ce32bd4 | ||
|
|
911f3439ea | ||
|
|
b02590d2b2 | ||
|
|
2d75b4b1f8 | ||
|
|
7e3f7d01c2 | ||
|
|
9d6ce26ea3 | ||
|
|
41713d42a2 | ||
|
|
8afc283410 | ||
|
|
b5c873077e | ||
|
|
20a4dd32eb | ||
|
|
fde0d44bc1 | ||
|
|
8fd91b6e83 | ||
|
|
8247fdd45b | ||
|
|
8c5859ba4d | ||
|
|
62ef6f59bb |
40
.github/workflows/deployment.yml
vendored
40
.github/workflows/deployment.yml
vendored
@@ -213,7 +213,7 @@ jobs:
|
||||
|
||||
- name: Configure AWS credentials
|
||||
if: startsWith(matrix.platform, 'macos-')
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -384,7 +384,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -458,7 +458,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -527,7 +527,7 @@ jobs:
|
||||
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # ratchet:runs-on/action@v2
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -597,7 +597,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -679,7 +679,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -756,7 +756,7 @@ jobs:
|
||||
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # ratchet:runs-on/action@v2
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -823,7 +823,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -896,7 +896,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -964,7 +964,7 @@ jobs:
|
||||
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # ratchet:runs-on/action@v2
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -1034,7 +1034,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -1107,7 +1107,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -1176,7 +1176,7 @@ jobs:
|
||||
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # ratchet:runs-on/action@v2
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -1246,7 +1246,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -1326,7 +1326,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -1400,7 +1400,7 @@ jobs:
|
||||
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # ratchet:runs-on/action@v2
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -1465,7 +1465,7 @@ jobs:
|
||||
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # ratchet:runs-on/action@v2
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -1520,7 +1520,7 @@ jobs:
|
||||
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # ratchet:runs-on/action@v2
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -1580,7 +1580,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -1637,7 +1637,7 @@ jobs:
|
||||
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # ratchet:runs-on/action@v2
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
|
||||
2
.github/workflows/pr-helm-chart-testing.yml
vendored
2
.github/workflows/pr-helm-chart-testing.yml
vendored
@@ -71,7 +71,7 @@ jobs:
|
||||
|
||||
- name: Create kind cluster
|
||||
if: steps.list-changed.outputs.changed == 'true'
|
||||
uses: helm/kind-action@92086f6be054225fa813e0a4b13787fc9088faab # ratchet:helm/kind-action@v1.13.0
|
||||
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # ratchet:helm/kind-action@v1.14.0
|
||||
|
||||
- name: Pre-install cluster status check
|
||||
if: steps.list-changed.outputs.changed == 'true'
|
||||
|
||||
2
.github/workflows/pr-playwright-tests.yml
vendored
2
.github/workflows/pr-playwright-tests.yml
vendored
@@ -461,7 +461,7 @@ jobs:
|
||||
# --- Visual Regression Diff ---
|
||||
- name: Configure AWS credentials
|
||||
if: always()
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
|
||||
4
.github/workflows/pr-quality-checks.yml
vendored
4
.github/workflows/pr-quality-checks.yml
vendored
@@ -38,9 +38,9 @@ jobs:
|
||||
- name: Install node dependencies
|
||||
working-directory: ./web
|
||||
run: npm ci
|
||||
- uses: j178/prek-action@9d6a3097e0c1865ecce00cfb89fe80f2ee91b547 # ratchet:j178/prek-action@v1
|
||||
- uses: j178/prek-action@0bb87d7f00b0c99306c8bcb8b8beba1eb581c037 # ratchet:j178/prek-action@v1
|
||||
with:
|
||||
prek-version: '0.2.21'
|
||||
prek-version: '0.3.4'
|
||||
extra-args: ${{ github.event_name == 'pull_request' && format('--from-ref {0} --to-ref {1}', github.event.pull_request.base.sha, github.event.pull_request.head.sha) || github.event_name == 'merge_group' && format('--from-ref {0} --to-ref {1}', github.event.merge_group.base_sha, github.event.merge_group.head_sha) || github.ref_name == 'main' && '--all-files' || '' }}
|
||||
- name: Check Actions
|
||||
uses: giner/check-actions@28d366c7cbbe235f9624a88aa31a628167eee28c # ratchet:giner/check-actions@v1.0.1
|
||||
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -158,7 +158,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -264,7 +264,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
|
||||
6
.github/workflows/sandbox-deployment.yml
vendored
6
.github/workflows/sandbox-deployment.yml
vendored
@@ -110,7 +110,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -180,7 +180,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
@@ -244,7 +244,7 @@ jobs:
|
||||
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # ratchet:runs-on/action@v2
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
|
||||
@@ -119,9 +119,10 @@ repos:
|
||||
]
|
||||
|
||||
- repo: https://github.com/golangci/golangci-lint
|
||||
rev: 9f61b0f53f80672872fced07b6874397c3ed197b # frozen: v2.7.2
|
||||
rev: 5d1e709b7be35cb2025444e19de266b056b7b7ee # frozen: v2.10.1
|
||||
hooks:
|
||||
- id: golangci-lint
|
||||
language_version: "1.26.0"
|
||||
entry: bash -c "find tools/ -name go.mod -print0 | xargs -0 -I{} bash -c 'cd \"$(dirname {})\" && golangci-lint run ./...'"
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
|
||||
@@ -2,13 +2,12 @@ from datetime import datetime
|
||||
from datetime import timedelta
|
||||
|
||||
import jwt
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Request
|
||||
|
||||
from onyx.configs.app_configs import DATA_PLANE_SECRET
|
||||
from onyx.configs.app_configs import EXPECTED_API_KEY
|
||||
from onyx.configs.app_configs import JWT_ALGORITHM
|
||||
from onyx.error_handling.error_codes import OnyxErrorCode
|
||||
from onyx.error_handling.exceptions import OnyxError
|
||||
from onyx.utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger()
|
||||
@@ -33,24 +32,22 @@ async def control_plane_dep(request: Request) -> None:
|
||||
api_key = request.headers.get("X-API-KEY")
|
||||
if api_key != EXPECTED_API_KEY:
|
||||
logger.warning("Invalid API key")
|
||||
raise OnyxError(OnyxErrorCode.UNAUTHENTICATED, "Invalid API key")
|
||||
raise HTTPException(status_code=401, detail="Invalid API key")
|
||||
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not auth_header or not auth_header.startswith("Bearer "):
|
||||
logger.warning("Invalid authorization header")
|
||||
raise OnyxError(OnyxErrorCode.UNAUTHENTICATED, "Invalid authorization header")
|
||||
raise HTTPException(status_code=401, detail="Invalid authorization header")
|
||||
|
||||
token = auth_header.split(" ")[1]
|
||||
try:
|
||||
payload = jwt.decode(token, DATA_PLANE_SECRET, algorithms=[JWT_ALGORITHM])
|
||||
if payload.get("scope") != "tenant:create":
|
||||
logger.warning("Insufficient permissions")
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.INSUFFICIENT_PERMISSIONS, "Insufficient permissions"
|
||||
)
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
except jwt.ExpiredSignatureError:
|
||||
logger.warning("Token has expired")
|
||||
raise OnyxError(OnyxErrorCode.TOKEN_EXPIRED, "Token has expired")
|
||||
raise HTTPException(status_code=401, detail="Token has expired")
|
||||
except jwt.InvalidTokenError:
|
||||
logger.warning("Invalid token")
|
||||
raise OnyxError(OnyxErrorCode.INVALID_TOKEN, "Invalid token")
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Response
|
||||
from fastapi_users import exceptions
|
||||
|
||||
@@ -11,8 +12,6 @@ from onyx.auth.users import get_redis_strategy
|
||||
from onyx.auth.users import User
|
||||
from onyx.db.engine.sql_engine import get_session_with_tenant
|
||||
from onyx.db.users import get_user_by_email
|
||||
from onyx.error_handling.error_codes import OnyxErrorCode
|
||||
from onyx.error_handling.exceptions import OnyxError
|
||||
from onyx.utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger()
|
||||
@@ -31,7 +30,7 @@ async def impersonate_user(
|
||||
except exceptions.UserNotExists:
|
||||
detail = f"User has no tenant mapping: {impersonate_request.email=}"
|
||||
logger.warning(detail)
|
||||
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, detail)
|
||||
raise HTTPException(status_code=422, detail=detail)
|
||||
|
||||
with get_session_with_tenant(tenant_id=tenant_id) as tenant_session:
|
||||
user_to_impersonate = get_user_by_email(
|
||||
@@ -42,7 +41,7 @@ async def impersonate_user(
|
||||
f"User not found in tenant: {impersonate_request.email=} {tenant_id=}"
|
||||
)
|
||||
logger.warning(detail)
|
||||
raise OnyxError(OnyxErrorCode.USER_NOT_FOUND, detail)
|
||||
raise HTTPException(status_code=422, detail=detail)
|
||||
|
||||
token = await get_redis_strategy().write_token(user_to_impersonate)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Response
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
@@ -17,8 +18,6 @@ from onyx.auth.users import User
|
||||
from onyx.configs.constants import ANONYMOUS_USER_COOKIE_NAME
|
||||
from onyx.configs.constants import FASTAPI_USERS_AUTH_COOKIE_NAME
|
||||
from onyx.db.engine.sql_engine import get_session_with_shared_schema
|
||||
from onyx.error_handling.error_codes import OnyxErrorCode
|
||||
from onyx.error_handling.exceptions import OnyxError
|
||||
from onyx.utils.logger import setup_logger
|
||||
from shared_configs.contextvars import get_current_tenant_id
|
||||
|
||||
@@ -34,7 +33,7 @@ async def get_anonymous_user_path_api(
|
||||
tenant_id = get_current_tenant_id()
|
||||
|
||||
if tenant_id is None:
|
||||
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Tenant not found")
|
||||
raise HTTPException(status_code=404, detail="Tenant not found")
|
||||
|
||||
with get_session_with_shared_schema() as db_session:
|
||||
current_path = get_anonymous_user_path(tenant_id, db_session)
|
||||
@@ -51,21 +50,21 @@ async def set_anonymous_user_path_api(
|
||||
try:
|
||||
validate_anonymous_user_path(anonymous_user_path)
|
||||
except ValueError as e:
|
||||
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
with get_session_with_shared_schema() as db_session:
|
||||
try:
|
||||
modify_anonymous_user_path(tenant_id, anonymous_user_path, db_session)
|
||||
except IntegrityError:
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.CONFLICT,
|
||||
"The anonymous user path is already in use. Please choose a different path.",
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="The anonymous user path is already in use. Please choose a different path.",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to modify anonymous user path: {str(e)}")
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.INTERNAL_ERROR,
|
||||
"An unexpected error occurred while modifying the anonymous user path",
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="An unexpected error occurred while modifying the anonymous user path",
|
||||
)
|
||||
|
||||
|
||||
@@ -78,10 +77,10 @@ async def login_as_anonymous_user(
|
||||
anonymous_user_path, db_session
|
||||
)
|
||||
if not tenant_id:
|
||||
raise OnyxError(OnyxErrorCode.NOT_FOUND, "Tenant not found")
|
||||
raise HTTPException(status_code=404, detail="Tenant not found")
|
||||
|
||||
if not anonymous_user_enabled(tenant_id=tenant_id):
|
||||
raise OnyxError(OnyxErrorCode.UNAUTHORIZED, "Anonymous user is not enabled")
|
||||
raise HTTPException(status_code=403, detail="Anonymous user is not enabled")
|
||||
|
||||
token = generate_anonymous_user_jwt_token(tenant_id)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import uuid
|
||||
import aiohttp # Async HTTP client
|
||||
import httpx
|
||||
import requests
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Request
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -40,8 +41,6 @@ from onyx.db.models import AvailableTenant
|
||||
from onyx.db.models import IndexModelStatus
|
||||
from onyx.db.models import SearchSettings
|
||||
from onyx.db.models import UserTenantMapping
|
||||
from onyx.error_handling.error_codes import OnyxErrorCode
|
||||
from onyx.error_handling.exceptions import OnyxError
|
||||
from onyx.llm.well_known_providers.auto_update_models import LLMRecommendations
|
||||
from onyx.llm.well_known_providers.constants import ANTHROPIC_PROVIDER_NAME
|
||||
from onyx.llm.well_known_providers.constants import OPENAI_PROVIDER_NAME
|
||||
@@ -117,9 +116,9 @@ async def get_or_provision_tenant(
|
||||
# If we've encountered an error, log and raise an exception
|
||||
error_msg = "Failed to provision tenant"
|
||||
logger.error(error_msg, exc_info=e)
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.INTERNAL_ERROR,
|
||||
"Failed to provision tenant. Please try again later.",
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Failed to provision tenant. Please try again later.",
|
||||
)
|
||||
|
||||
|
||||
@@ -145,18 +144,18 @@ async def create_tenant(
|
||||
await rollback_tenant_provisioning(tenant_id)
|
||||
except Exception:
|
||||
logger.exception(f"Failed to rollback tenant provisioning for {tenant_id}")
|
||||
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, "Failed to provision tenant.")
|
||||
raise HTTPException(status_code=500, detail="Failed to provision tenant.")
|
||||
|
||||
return tenant_id
|
||||
|
||||
|
||||
async def provision_tenant(tenant_id: str, email: str) -> None:
|
||||
if not MULTI_TENANT:
|
||||
raise OnyxError(OnyxErrorCode.UNAUTHORIZED, "Multi-tenancy is not enabled")
|
||||
raise HTTPException(status_code=403, detail="Multi-tenancy is not enabled")
|
||||
|
||||
if user_owns_a_tenant(email):
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.CONFLICT, "User already belongs to an organization"
|
||||
raise HTTPException(
|
||||
status_code=409, detail="User already belongs to an organization"
|
||||
)
|
||||
|
||||
logger.debug(f"Provisioning tenant {tenant_id} for user {email}")
|
||||
@@ -176,8 +175,8 @@ async def provision_tenant(tenant_id: str, email: str) -> None:
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to create tenant {tenant_id}")
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.INTERNAL_ERROR, f"Failed to create tenant: {str(e)}"
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to create tenant: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import httpx
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import Header
|
||||
from fastapi import HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ee.onyx.configs.app_configs import LICENSE_ENFORCEMENT_ENABLED
|
||||
@@ -35,8 +36,6 @@ from ee.onyx.server.tenants.access import generate_data_plane_token
|
||||
from ee.onyx.utils.license import is_license_valid
|
||||
from ee.onyx.utils.license import verify_license_signature
|
||||
from onyx.configs.app_configs import CONTROL_PLANE_API_BASE_URL
|
||||
from onyx.error_handling.error_codes import OnyxErrorCode
|
||||
from onyx.error_handling.exceptions import OnyxError
|
||||
from onyx.utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger()
|
||||
@@ -47,9 +46,9 @@ router = APIRouter(prefix="/proxy")
|
||||
def _check_license_enforcement_enabled() -> None:
|
||||
"""Ensure LICENSE_ENFORCEMENT_ENABLED is true (proxy endpoints only work on cloud DP)."""
|
||||
if not LICENSE_ENFORCEMENT_ENABLED:
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.NOT_IMPLEMENTED,
|
||||
"Proxy endpoints are only available on cloud data plane",
|
||||
raise HTTPException(
|
||||
status_code=501,
|
||||
detail="Proxy endpoints are only available on cloud data plane",
|
||||
)
|
||||
|
||||
|
||||
@@ -82,9 +81,8 @@ def _extract_license_from_header(
|
||||
"""
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
if required:
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.UNAUTHENTICATED,
|
||||
"Missing or invalid authorization header",
|
||||
raise HTTPException(
|
||||
status_code=401, detail="Missing or invalid authorization header"
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -112,10 +110,10 @@ def verify_license_auth(
|
||||
try:
|
||||
payload = verify_license_signature(license_data)
|
||||
except ValueError as e:
|
||||
raise OnyxError(OnyxErrorCode.UNAUTHENTICATED, f"Invalid license: {e}")
|
||||
raise HTTPException(status_code=401, detail=f"Invalid license: {e}")
|
||||
|
||||
if not allow_expired and not is_license_valid(payload):
|
||||
raise OnyxError(OnyxErrorCode.TOKEN_EXPIRED, "License has expired")
|
||||
raise HTTPException(status_code=401, detail="License has expired")
|
||||
|
||||
return payload
|
||||
|
||||
@@ -199,12 +197,12 @@ async def forward_to_control_plane(
|
||||
except Exception:
|
||||
pass
|
||||
logger.error(f"Control plane returned {status_code}: {detail}")
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.BAD_GATEWAY, detail, status_code_override=status_code
|
||||
)
|
||||
raise HTTPException(status_code=status_code, detail=detail)
|
||||
except httpx.RequestError:
|
||||
logger.exception("Failed to connect to control plane")
|
||||
raise OnyxError(OnyxErrorCode.BAD_GATEWAY, "Failed to connect to control plane")
|
||||
raise HTTPException(
|
||||
status_code=502, detail="Failed to connect to control plane"
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -296,9 +294,9 @@ async def proxy_claim_license(
|
||||
|
||||
if not tenant_id or not license_data:
|
||||
logger.error(f"Control plane returned incomplete claim response: {result}")
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.BAD_GATEWAY,
|
||||
"Control plane returned incomplete license data",
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail="Control plane returned incomplete license data",
|
||||
)
|
||||
|
||||
return ClaimLicenseResponse(
|
||||
@@ -328,7 +326,7 @@ async def proxy_create_customer_portal_session(
|
||||
# tenant_id is a required field in LicensePayload (Pydantic validates this),
|
||||
# but we check explicitly for defense in depth
|
||||
if not license_payload.tenant_id:
|
||||
raise OnyxError(OnyxErrorCode.UNAUTHENTICATED, "License missing tenant_id")
|
||||
raise HTTPException(status_code=401, detail="License missing tenant_id")
|
||||
|
||||
tenant_id = license_payload.tenant_id
|
||||
|
||||
@@ -369,7 +367,7 @@ async def proxy_billing_information(
|
||||
# tenant_id is a required field in LicensePayload (Pydantic validates this),
|
||||
# but we check explicitly for defense in depth
|
||||
if not license_payload.tenant_id:
|
||||
raise OnyxError(OnyxErrorCode.UNAUTHENTICATED, "License missing tenant_id")
|
||||
raise HTTPException(status_code=401, detail="License missing tenant_id")
|
||||
|
||||
tenant_id = license_payload.tenant_id
|
||||
|
||||
@@ -400,12 +398,12 @@ async def proxy_license_fetch(
|
||||
# tenant_id is a required field in LicensePayload (Pydantic validates this),
|
||||
# but we check explicitly for defense in depth
|
||||
if not license_payload.tenant_id:
|
||||
raise OnyxError(OnyxErrorCode.UNAUTHENTICATED, "License missing tenant_id")
|
||||
raise HTTPException(status_code=401, detail="License missing tenant_id")
|
||||
|
||||
if tenant_id != license_payload.tenant_id:
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.UNAUTHORIZED,
|
||||
"Cannot fetch license for a different tenant",
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Cannot fetch license for a different tenant",
|
||||
)
|
||||
|
||||
result = await forward_to_control_plane("GET", f"/license/{tenant_id}")
|
||||
@@ -413,9 +411,9 @@ async def proxy_license_fetch(
|
||||
license_data = result.get("license")
|
||||
if not license_data:
|
||||
logger.error(f"Control plane returned incomplete license response: {result}")
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.BAD_GATEWAY,
|
||||
"Control plane returned incomplete license data",
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail="Control plane returned incomplete license data",
|
||||
)
|
||||
|
||||
# Return license to caller - self-hosted instance stores it via /api/license/claim
|
||||
@@ -434,7 +432,7 @@ async def proxy_seat_update(
|
||||
Returns the regenerated license in the response for the caller to store.
|
||||
"""
|
||||
if not license_payload.tenant_id:
|
||||
raise OnyxError(OnyxErrorCode.UNAUTHENTICATED, "License missing tenant_id")
|
||||
raise HTTPException(status_code=401, detail="License missing tenant_id")
|
||||
|
||||
tenant_id = license_payload.tenant_id
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ee.onyx.server.tenants.provisioning import delete_user_from_control_plane
|
||||
@@ -11,8 +12,6 @@ from onyx.db.auth import get_user_count
|
||||
from onyx.db.engine.sql_engine import get_session
|
||||
from onyx.db.users import delete_user_from_db
|
||||
from onyx.db.users import get_user_by_email
|
||||
from onyx.error_handling.error_codes import OnyxErrorCode
|
||||
from onyx.error_handling.exceptions import OnyxError
|
||||
from onyx.server.manage.models import UserByEmail
|
||||
from onyx.utils.logger import setup_logger
|
||||
from shared_configs.contextvars import get_current_tenant_id
|
||||
@@ -31,14 +30,13 @@ async def leave_organization(
|
||||
tenant_id = get_current_tenant_id()
|
||||
|
||||
if current_user.email != user_email.user_email:
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.INSUFFICIENT_PERMISSIONS,
|
||||
"You can only leave the organization as yourself",
|
||||
raise HTTPException(
|
||||
status_code=403, detail="You can only leave the organization as yourself"
|
||||
)
|
||||
|
||||
user_to_delete = get_user_by_email(user_email.user_email, db_session)
|
||||
if user_to_delete is None:
|
||||
raise OnyxError(OnyxErrorCode.USER_NOT_FOUND, "User not found")
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
num_admin_users = await get_user_count(only_admin_users=True)
|
||||
|
||||
@@ -55,9 +53,9 @@ async def leave_organization(
|
||||
logger.exception(
|
||||
f"Failed to delete user from control plane for tenant {tenant_id}: {e}"
|
||||
)
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.INTERNAL_ERROR,
|
||||
f"Failed to remove user from control plane: {str(e)}",
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to remove user from control plane: {str(e)}",
|
||||
)
|
||||
|
||||
db_session.expunge(user_to_delete)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
|
||||
from ee.onyx.server.tenants.models import ApproveUserRequest
|
||||
from ee.onyx.server.tenants.models import PendingUserSnapshot
|
||||
@@ -12,8 +13,6 @@ from onyx.auth.invited_users import get_pending_users
|
||||
from onyx.auth.users import current_admin_user
|
||||
from onyx.auth.users import current_user
|
||||
from onyx.auth.users import User
|
||||
from onyx.error_handling.error_codes import OnyxErrorCode
|
||||
from onyx.error_handling.exceptions import OnyxError
|
||||
from onyx.utils.logger import setup_logger
|
||||
from shared_configs.contextvars import get_current_tenant_id
|
||||
|
||||
@@ -33,7 +32,7 @@ async def request_invite(
|
||||
logger.exception(
|
||||
f"Failed to invite self to tenant {invite_request.tenant_id}: {e}"
|
||||
)
|
||||
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/users/pending")
|
||||
@@ -65,7 +64,7 @@ async def accept_invite(
|
||||
accept_user_invite(user.email, invite_request.tenant_id)
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to accept invite: {str(e)}")
|
||||
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, "Failed to accept invitation")
|
||||
raise HTTPException(status_code=500, detail="Failed to accept invitation")
|
||||
|
||||
|
||||
@router.post("/users/invite/deny")
|
||||
@@ -80,4 +79,4 @@ async def deny_invite(
|
||||
deny_user_invite(user.email, invite_request.tenant_id)
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to deny invite: {str(e)}")
|
||||
raise OnyxError(OnyxErrorCode.INTERNAL_ERROR, "Failed to deny invitation")
|
||||
raise HTTPException(status_code=500, detail="Failed to deny invitation")
|
||||
|
||||
@@ -36,7 +36,6 @@ from onyx.db.memory import add_memory
|
||||
from onyx.db.memory import update_memory_at_index
|
||||
from onyx.db.memory import UserMemoryContext
|
||||
from onyx.db.models import Persona
|
||||
from onyx.llm.constants import LlmProviderNames
|
||||
from onyx.llm.interfaces import LLM
|
||||
from onyx.llm.interfaces import LLMUserIdentity
|
||||
from onyx.llm.interfaces import ToolChoiceOptions
|
||||
@@ -84,28 +83,6 @@ def _looks_like_xml_tool_call_payload(text: str | None) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _should_keep_bedrock_tool_definitions(
|
||||
llm: object, simple_chat_history: list[ChatMessageSimple]
|
||||
) -> bool:
|
||||
"""Bedrock requires tool config when history includes toolUse/toolResult blocks."""
|
||||
model_provider = getattr(getattr(llm, "config", None), "model_provider", None)
|
||||
if model_provider not in {
|
||||
LlmProviderNames.BEDROCK,
|
||||
LlmProviderNames.BEDROCK_CONVERSE,
|
||||
}:
|
||||
return False
|
||||
|
||||
return any(
|
||||
(
|
||||
msg.message_type == MessageType.ASSISTANT
|
||||
and msg.tool_calls
|
||||
and len(msg.tool_calls) > 0
|
||||
)
|
||||
or msg.message_type == MessageType.TOOL_CALL_RESPONSE
|
||||
for msg in simple_chat_history
|
||||
)
|
||||
|
||||
|
||||
def _try_fallback_tool_extraction(
|
||||
llm_step_result: LlmStepResult,
|
||||
tool_choice: ToolChoiceOptions,
|
||||
@@ -686,12 +663,7 @@ def run_llm_loop(
|
||||
elif out_of_cycles or ran_image_gen:
|
||||
# Last cycle, no tools allowed, just answer!
|
||||
tool_choice = ToolChoiceOptions.NONE
|
||||
# Bedrock requires tool config in requests that include toolUse/toolResult history.
|
||||
final_tools = (
|
||||
tools
|
||||
if _should_keep_bedrock_tool_definitions(llm, simple_chat_history)
|
||||
else []
|
||||
)
|
||||
final_tools = []
|
||||
else:
|
||||
tool_choice = ToolChoiceOptions.AUTO
|
||||
final_tools = tools
|
||||
|
||||
@@ -92,6 +92,98 @@ def _prompt_to_dicts(prompt: LanguageModelInput) -> list[dict[str, Any]]:
|
||||
return [prompt.model_dump(exclude_none=True)]
|
||||
|
||||
|
||||
def _normalize_content(raw: Any) -> str:
|
||||
"""Normalize a message content field to a plain string.
|
||||
|
||||
Content can be a string, None, or a list of content-block dicts
|
||||
(e.g. [{"type": "text", "text": "..."}]).
|
||||
"""
|
||||
if raw is None:
|
||||
return ""
|
||||
if isinstance(raw, str):
|
||||
return raw
|
||||
if isinstance(raw, list):
|
||||
return "\n".join(
|
||||
block.get("text", "") if isinstance(block, dict) else str(block)
|
||||
for block in raw
|
||||
)
|
||||
return str(raw)
|
||||
|
||||
|
||||
def _strip_tool_content_from_messages(
|
||||
messages: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Convert tool-related messages to plain text.
|
||||
|
||||
Bedrock's Converse API requires toolConfig when messages contain
|
||||
toolUse/toolResult content blocks. When no tools are provided for the
|
||||
current request, we must convert any tool-related history into plain text
|
||||
to avoid the "toolConfig field must be defined" error.
|
||||
|
||||
This is the same approach used by _OllamaHistoryMessageFormatter.
|
||||
"""
|
||||
result: list[dict[str, Any]] = []
|
||||
for msg in messages:
|
||||
role = msg.get("role")
|
||||
tool_calls = msg.get("tool_calls")
|
||||
|
||||
if role == "assistant" and tool_calls:
|
||||
# Convert structured tool calls to text representation
|
||||
tool_call_lines = []
|
||||
for tc in tool_calls:
|
||||
func = tc.get("function", {})
|
||||
name = func.get("name", "unknown")
|
||||
args = func.get("arguments", "{}")
|
||||
tc_id = tc.get("id", "")
|
||||
tool_call_lines.append(
|
||||
f"[Tool Call] name={name} id={tc_id} args={args}"
|
||||
)
|
||||
|
||||
existing_content = _normalize_content(msg.get("content"))
|
||||
parts = (
|
||||
[existing_content] + tool_call_lines
|
||||
if existing_content
|
||||
else tool_call_lines
|
||||
)
|
||||
new_msg = {
|
||||
"role": "assistant",
|
||||
"content": "\n".join(parts),
|
||||
}
|
||||
result.append(new_msg)
|
||||
|
||||
elif role == "tool":
|
||||
# Convert tool response to user message with text content
|
||||
tool_call_id = msg.get("tool_call_id", "")
|
||||
content = _normalize_content(msg.get("content"))
|
||||
tool_result_text = f"[Tool Result] id={tool_call_id}\n{content}"
|
||||
# Merge into previous user message if it is also a converted
|
||||
# tool result to avoid consecutive user messages (Bedrock requires
|
||||
# strict user/assistant alternation).
|
||||
if (
|
||||
result
|
||||
and result[-1]["role"] == "user"
|
||||
and "[Tool Result]" in result[-1].get("content", "")
|
||||
):
|
||||
result[-1]["content"] += "\n\n" + tool_result_text
|
||||
else:
|
||||
result.append({"role": "user", "content": tool_result_text})
|
||||
|
||||
else:
|
||||
result.append(msg)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _messages_contain_tool_content(messages: list[dict[str, Any]]) -> bool:
|
||||
"""Check if any messages contain tool-related content blocks."""
|
||||
for msg in messages:
|
||||
if msg.get("role") == "tool":
|
||||
return True
|
||||
if msg.get("role") == "assistant" and msg.get("tool_calls"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _is_vertex_model_rejecting_output_config(model_name: str) -> bool:
|
||||
normalized_model_name = model_name.lower()
|
||||
return any(
|
||||
@@ -404,13 +496,30 @@ class LitellmLLM(LLM):
|
||||
else nullcontext()
|
||||
)
|
||||
with env_ctx:
|
||||
messages = _prompt_to_dicts(prompt)
|
||||
|
||||
# Bedrock's Converse API requires toolConfig when messages
|
||||
# contain toolUse/toolResult content blocks. When no tools are
|
||||
# provided for this request but the history contains tool
|
||||
# content from previous turns, strip it to plain text.
|
||||
is_bedrock = self._model_provider in {
|
||||
LlmProviderNames.BEDROCK,
|
||||
LlmProviderNames.BEDROCK_CONVERSE,
|
||||
}
|
||||
if (
|
||||
is_bedrock
|
||||
and not tools
|
||||
and _messages_contain_tool_content(messages)
|
||||
):
|
||||
messages = _strip_tool_content_from_messages(messages)
|
||||
|
||||
response = litellm.completion(
|
||||
mock_response=get_llm_mock_response() or MOCK_LLM_RESPONSE,
|
||||
model=model,
|
||||
base_url=self._api_base or None,
|
||||
api_version=self._api_version or None,
|
||||
custom_llm_provider=self._custom_llm_provider or None,
|
||||
messages=_prompt_to_dicts(prompt),
|
||||
messages=messages,
|
||||
tools=tools,
|
||||
tool_choice=tool_choice,
|
||||
stream=stream,
|
||||
|
||||
@@ -961,9 +961,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "1.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
|
||||
"integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==",
|
||||
"version": "1.19.10",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.10.tgz",
|
||||
"integrity": "sha512-hZ7nOssGqRgyV3FVVQdfi+U4q02uB23bpnYpdvNXkYTRRyWx84b7yf1ans+dnJ/7h41sGL3CeQTfO+ZGxuO+Iw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.14.1"
|
||||
@@ -1573,27 +1573,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/brace-expansion": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
|
||||
"integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@isaacs/balanced-match": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
@@ -1680,9 +1659,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk/node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@@ -3855,6 +3834,27 @@
|
||||
"path-browserify": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ts-morph/common/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@ts-morph/common/node_modules/brace-expansion": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
|
||||
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@ts-morph/common/node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
@@ -3884,15 +3884,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ts-morph/common/node_modules/minimatch": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
|
||||
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
|
||||
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
"brace-expansion": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
@@ -4234,13 +4234,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -4619,9 +4619,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4653,9 +4653,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats/node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@@ -8831,9 +8831,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -65,7 +65,7 @@ attrs==25.4.0
|
||||
# jsonschema
|
||||
# referencing
|
||||
# zeep
|
||||
authlib==1.6.6
|
||||
authlib==1.6.7
|
||||
# via fastmcp
|
||||
babel==2.17.0
|
||||
# via courlan
|
||||
@@ -109,9 +109,7 @@ brotli==1.2.0
|
||||
bytecode==0.17.0
|
||||
# via ddtrace
|
||||
cachetools==6.2.2
|
||||
# via
|
||||
# google-auth
|
||||
# py-key-value-aio
|
||||
# via py-key-value-aio
|
||||
caio==0.9.25
|
||||
# via aiofile
|
||||
celery==5.5.1
|
||||
@@ -190,6 +188,7 @@ courlan==1.3.2
|
||||
cryptography==46.0.5
|
||||
# via
|
||||
# authlib
|
||||
# google-auth
|
||||
# msal
|
||||
# msoffcrypto-tool
|
||||
# pdfminer-six
|
||||
@@ -306,7 +305,7 @@ google-api-core==2.28.1
|
||||
# google-cloud-storage
|
||||
google-api-python-client==2.86.0
|
||||
# via onyx
|
||||
google-auth==2.43.0
|
||||
google-auth==2.48.0
|
||||
# via
|
||||
# google-api-core
|
||||
# google-api-python-client
|
||||
@@ -325,7 +324,7 @@ google-auth-httplib2==0.1.0
|
||||
# onyx
|
||||
google-auth-oauthlib==1.0.0
|
||||
# via onyx
|
||||
google-cloud-aiplatform==1.121.0
|
||||
google-cloud-aiplatform==1.133.0
|
||||
# via onyx
|
||||
google-cloud-bigquery==3.38.0
|
||||
# via google-cloud-aiplatform
|
||||
@@ -1002,9 +1001,7 @@ sendgrid==6.12.5
|
||||
sentry-sdk==2.14.0
|
||||
# via onyx
|
||||
shapely==2.0.6
|
||||
# via
|
||||
# google-cloud-aiplatform
|
||||
# onyx
|
||||
# via onyx
|
||||
shellingham==1.5.4
|
||||
# via typer
|
||||
simple-salesforce==1.12.6
|
||||
|
||||
@@ -59,8 +59,6 @@ botocore==1.39.11
|
||||
# s3transfer
|
||||
brotli==1.2.0
|
||||
# via onyx
|
||||
cachetools==6.2.2
|
||||
# via google-auth
|
||||
celery-types==0.19.0
|
||||
# via onyx
|
||||
certifi==2025.11.12
|
||||
@@ -100,7 +98,9 @@ comm==0.2.3
|
||||
contourpy==1.3.3
|
||||
# via matplotlib
|
||||
cryptography==46.0.5
|
||||
# via pyjwt
|
||||
# via
|
||||
# google-auth
|
||||
# pyjwt
|
||||
cycler==0.12.1
|
||||
# via matplotlib
|
||||
debugpy==1.8.17
|
||||
@@ -152,7 +152,7 @@ google-api-core==2.28.1
|
||||
# google-cloud-core
|
||||
# google-cloud-resource-manager
|
||||
# google-cloud-storage
|
||||
google-auth==2.43.0
|
||||
google-auth==2.48.0
|
||||
# via
|
||||
# google-api-core
|
||||
# google-cloud-aiplatform
|
||||
@@ -162,7 +162,7 @@ google-auth==2.43.0
|
||||
# google-cloud-storage
|
||||
# google-genai
|
||||
# kubernetes
|
||||
google-cloud-aiplatform==1.121.0
|
||||
google-cloud-aiplatform==1.133.0
|
||||
# via onyx
|
||||
google-cloud-bigquery==3.38.0
|
||||
# via google-cloud-aiplatform
|
||||
@@ -311,13 +311,12 @@ numpy==2.4.1
|
||||
# contourpy
|
||||
# matplotlib
|
||||
# pandas-stubs
|
||||
# shapely
|
||||
# voyageai
|
||||
oauthlib==3.2.2
|
||||
# via
|
||||
# kubernetes
|
||||
# requests-oauthlib
|
||||
onyx-devtools==0.6.2
|
||||
onyx-devtools==0.6.3
|
||||
# via onyx
|
||||
openai==2.14.0
|
||||
# via
|
||||
@@ -510,8 +509,6 @@ s3transfer==0.13.1
|
||||
# via boto3
|
||||
sentry-sdk==2.14.0
|
||||
# via onyx
|
||||
shapely==2.0.6
|
||||
# via google-cloud-aiplatform
|
||||
six==1.17.0
|
||||
# via
|
||||
# kubernetes
|
||||
|
||||
@@ -53,8 +53,6 @@ botocore==1.39.11
|
||||
# s3transfer
|
||||
brotli==1.2.0
|
||||
# via onyx
|
||||
cachetools==6.2.2
|
||||
# via google-auth
|
||||
certifi==2025.11.12
|
||||
# via
|
||||
# httpcore
|
||||
@@ -79,7 +77,9 @@ colorama==0.4.6 ; sys_platform == 'win32'
|
||||
# click
|
||||
# tqdm
|
||||
cryptography==46.0.5
|
||||
# via pyjwt
|
||||
# via
|
||||
# google-auth
|
||||
# pyjwt
|
||||
decorator==5.2.1
|
||||
# via retry
|
||||
discord-py==2.4.0
|
||||
@@ -111,7 +111,7 @@ google-api-core==2.28.1
|
||||
# google-cloud-core
|
||||
# google-cloud-resource-manager
|
||||
# google-cloud-storage
|
||||
google-auth==2.43.0
|
||||
google-auth==2.48.0
|
||||
# via
|
||||
# google-api-core
|
||||
# google-cloud-aiplatform
|
||||
@@ -121,7 +121,7 @@ google-auth==2.43.0
|
||||
# google-cloud-storage
|
||||
# google-genai
|
||||
# kubernetes
|
||||
google-cloud-aiplatform==1.121.0
|
||||
google-cloud-aiplatform==1.133.0
|
||||
# via onyx
|
||||
google-cloud-bigquery==3.38.0
|
||||
# via google-cloud-aiplatform
|
||||
@@ -221,9 +221,7 @@ multidict==6.7.0
|
||||
# aiohttp
|
||||
# yarl
|
||||
numpy==2.4.1
|
||||
# via
|
||||
# shapely
|
||||
# voyageai
|
||||
# via voyageai
|
||||
oauthlib==3.2.2
|
||||
# via
|
||||
# kubernetes
|
||||
@@ -345,8 +343,6 @@ s3transfer==0.13.1
|
||||
# via boto3
|
||||
sentry-sdk==2.14.0
|
||||
# via onyx
|
||||
shapely==2.0.6
|
||||
# via google-cloud-aiplatform
|
||||
six==1.17.0
|
||||
# via
|
||||
# kubernetes
|
||||
|
||||
@@ -57,8 +57,6 @@ botocore==1.39.11
|
||||
# s3transfer
|
||||
brotli==1.2.0
|
||||
# via onyx
|
||||
cachetools==6.2.2
|
||||
# via google-auth
|
||||
celery==5.5.1
|
||||
# via sentry-sdk
|
||||
certifi==2025.11.12
|
||||
@@ -95,7 +93,9 @@ colorama==0.4.6 ; sys_platform == 'win32'
|
||||
# click
|
||||
# tqdm
|
||||
cryptography==46.0.5
|
||||
# via pyjwt
|
||||
# via
|
||||
# google-auth
|
||||
# pyjwt
|
||||
decorator==5.2.1
|
||||
# via retry
|
||||
discord-py==2.4.0
|
||||
@@ -136,7 +136,7 @@ google-api-core==2.28.1
|
||||
# google-cloud-core
|
||||
# google-cloud-resource-manager
|
||||
# google-cloud-storage
|
||||
google-auth==2.43.0
|
||||
google-auth==2.48.0
|
||||
# via
|
||||
# google-api-core
|
||||
# google-cloud-aiplatform
|
||||
@@ -146,7 +146,7 @@ google-auth==2.43.0
|
||||
# google-cloud-storage
|
||||
# google-genai
|
||||
# kubernetes
|
||||
google-cloud-aiplatform==1.121.0
|
||||
google-cloud-aiplatform==1.133.0
|
||||
# via onyx
|
||||
google-cloud-bigquery==3.38.0
|
||||
# via google-cloud-aiplatform
|
||||
@@ -263,7 +263,6 @@ numpy==2.4.1
|
||||
# onyx
|
||||
# scikit-learn
|
||||
# scipy
|
||||
# shapely
|
||||
# transformers
|
||||
# voyageai
|
||||
nvidia-cublas-cu12==12.8.4.1 ; platform_machine == 'x86_64' and sys_platform == 'linux'
|
||||
@@ -452,8 +451,6 @@ sentry-sdk==2.14.0
|
||||
# via onyx
|
||||
setuptools==80.9.0 ; python_full_version >= '3.12'
|
||||
# via torch
|
||||
shapely==2.0.6
|
||||
# via google-cloud-aiplatform
|
||||
six==1.17.0
|
||||
# via
|
||||
# kubernetes
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from onyx.chat.llm_loop import _should_keep_bedrock_tool_definitions
|
||||
from onyx.chat.llm_loop import _try_fallback_tool_extraction
|
||||
from onyx.chat.llm_loop import construct_message_history
|
||||
from onyx.chat.models import ChatLoadedFile
|
||||
@@ -14,22 +13,11 @@ from onyx.chat.models import LlmStepResult
|
||||
from onyx.chat.models import ToolCallSimple
|
||||
from onyx.configs.constants import MessageType
|
||||
from onyx.file_store.models import ChatFileType
|
||||
from onyx.llm.constants import LlmProviderNames
|
||||
from onyx.llm.interfaces import ToolChoiceOptions
|
||||
from onyx.server.query_and_chat.placement import Placement
|
||||
from onyx.tools.models import ToolCallKickoff
|
||||
|
||||
|
||||
class _StubConfig:
|
||||
def __init__(self, model_provider: str) -> None:
|
||||
self.model_provider = model_provider
|
||||
|
||||
|
||||
class _StubLLM:
|
||||
def __init__(self, model_provider: str) -> None:
|
||||
self.config = _StubConfig(model_provider=model_provider)
|
||||
|
||||
|
||||
def create_message(
|
||||
content: str, message_type: MessageType, token_count: int | None = None
|
||||
) -> ChatMessageSimple:
|
||||
@@ -946,37 +934,6 @@ class TestForgottenFileMetadata:
|
||||
assert "moby_dick.txt" in forgotten.message
|
||||
|
||||
|
||||
class TestBedrockToolConfigGuard:
|
||||
def test_bedrock_with_tool_history_keeps_tool_definitions(self) -> None:
|
||||
llm = _StubLLM(LlmProviderNames.BEDROCK)
|
||||
history = [
|
||||
create_message("Question", MessageType.USER, 5),
|
||||
create_assistant_with_tool_call("tc_1", "search", 5),
|
||||
create_tool_response("tc_1", "Tool output", 5),
|
||||
]
|
||||
|
||||
assert _should_keep_bedrock_tool_definitions(llm, history) is True
|
||||
|
||||
def test_bedrock_without_tool_history_does_not_keep_tool_definitions(self) -> None:
|
||||
llm = _StubLLM(LlmProviderNames.BEDROCK)
|
||||
history = [
|
||||
create_message("Question", MessageType.USER, 5),
|
||||
create_message("Answer", MessageType.ASSISTANT, 5),
|
||||
]
|
||||
|
||||
assert _should_keep_bedrock_tool_definitions(llm, history) is False
|
||||
|
||||
def test_non_bedrock_with_tool_history_does_not_keep_tool_definitions(self) -> None:
|
||||
llm = _StubLLM(LlmProviderNames.OPENAI)
|
||||
history = [
|
||||
create_message("Question", MessageType.USER, 5),
|
||||
create_assistant_with_tool_call("tc_1", "search", 5),
|
||||
create_tool_response("tc_1", "Tool output", 5),
|
||||
]
|
||||
|
||||
assert _should_keep_bedrock_tool_definitions(llm, history) is False
|
||||
|
||||
|
||||
class TestFallbackToolExtraction:
|
||||
def _tool_defs(self) -> list[dict]:
|
||||
return [
|
||||
|
||||
@@ -1214,3 +1214,218 @@ def test_multithreaded_invoke_without_custom_config_skips_env_lock() -> None:
|
||||
|
||||
# The env lock context manager should never have been called
|
||||
mock_env_lock.assert_not_called()
|
||||
|
||||
|
||||
# ---- Tests for Bedrock tool content stripping ----
|
||||
|
||||
|
||||
def test_messages_contain_tool_content_with_tool_role() -> None:
|
||||
from onyx.llm.multi_llm import _messages_contain_tool_content
|
||||
|
||||
messages: list[dict[str, Any]] = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "I'll search for that."},
|
||||
{"role": "tool", "content": "search results", "tool_call_id": "tc_1"},
|
||||
]
|
||||
assert _messages_contain_tool_content(messages) is True
|
||||
|
||||
|
||||
def test_messages_contain_tool_content_with_tool_calls() -> None:
|
||||
from onyx.llm.multi_llm import _messages_contain_tool_content
|
||||
|
||||
messages: list[dict[str, Any]] = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "tc_1",
|
||||
"type": "function",
|
||||
"function": {"name": "search", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
assert _messages_contain_tool_content(messages) is True
|
||||
|
||||
|
||||
def test_messages_contain_tool_content_without_tools() -> None:
|
||||
from onyx.llm.multi_llm import _messages_contain_tool_content
|
||||
|
||||
messages: list[dict[str, Any]] = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "Hi there!"},
|
||||
]
|
||||
assert _messages_contain_tool_content(messages) is False
|
||||
|
||||
|
||||
def test_strip_tool_content_converts_assistant_tool_calls_to_text() -> None:
|
||||
from onyx.llm.multi_llm import _strip_tool_content_from_messages
|
||||
|
||||
messages: list[dict[str, Any]] = [
|
||||
{"role": "user", "content": "Search for cats"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Let me search.",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "tc_1",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search",
|
||||
"arguments": '{"query": "cats"}',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "Found 3 results about cats.",
|
||||
"tool_call_id": "tc_1",
|
||||
},
|
||||
{"role": "assistant", "content": "Here are the results."},
|
||||
]
|
||||
|
||||
result = _strip_tool_content_from_messages(messages)
|
||||
|
||||
assert len(result) == 4
|
||||
|
||||
# First message unchanged
|
||||
assert result[0] == {"role": "user", "content": "Search for cats"}
|
||||
|
||||
# Assistant with tool calls → plain text
|
||||
assert result[1]["role"] == "assistant"
|
||||
assert "tool_calls" not in result[1]
|
||||
assert "Let me search." in result[1]["content"]
|
||||
assert "[Tool Call]" in result[1]["content"]
|
||||
assert "search" in result[1]["content"]
|
||||
assert "tc_1" in result[1]["content"]
|
||||
|
||||
# Tool response → user message
|
||||
assert result[2]["role"] == "user"
|
||||
assert "[Tool Result]" in result[2]["content"]
|
||||
assert "tc_1" in result[2]["content"]
|
||||
assert "Found 3 results about cats." in result[2]["content"]
|
||||
|
||||
# Final assistant message unchanged
|
||||
assert result[3] == {"role": "assistant", "content": "Here are the results."}
|
||||
|
||||
|
||||
def test_strip_tool_content_handles_assistant_with_no_text_content() -> None:
|
||||
from onyx.llm.multi_llm import _strip_tool_content_from_messages
|
||||
|
||||
messages: list[dict[str, Any]] = [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "tc_1",
|
||||
"type": "function",
|
||||
"function": {"name": "search", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
result = _strip_tool_content_from_messages(messages)
|
||||
assert result[0]["role"] == "assistant"
|
||||
assert "[Tool Call]" in result[0]["content"]
|
||||
assert "tool_calls" not in result[0]
|
||||
|
||||
|
||||
def test_strip_tool_content_passes_through_non_tool_messages() -> None:
|
||||
from onyx.llm.multi_llm import _strip_tool_content_from_messages
|
||||
|
||||
messages: list[dict[str, Any]] = [
|
||||
{"role": "system", "content": "You are helpful."},
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "Hi!"},
|
||||
]
|
||||
|
||||
result = _strip_tool_content_from_messages(messages)
|
||||
assert result == messages
|
||||
|
||||
|
||||
def test_strip_tool_content_handles_list_content_blocks() -> None:
|
||||
from onyx.llm.multi_llm import _strip_tool_content_from_messages
|
||||
|
||||
messages: list[dict[str, Any]] = [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [{"type": "text", "text": "Searching now."}],
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "tc_1",
|
||||
"type": "function",
|
||||
"function": {"name": "search", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": [
|
||||
{"type": "text", "text": "result A"},
|
||||
{"type": "text", "text": "result B"},
|
||||
],
|
||||
"tool_call_id": "tc_1",
|
||||
},
|
||||
]
|
||||
|
||||
result = _strip_tool_content_from_messages(messages)
|
||||
|
||||
# Assistant: list content flattened + tool call appended
|
||||
assert result[0]["role"] == "assistant"
|
||||
assert "Searching now." in result[0]["content"]
|
||||
assert "[Tool Call]" in result[0]["content"]
|
||||
assert isinstance(result[0]["content"], str)
|
||||
|
||||
# Tool: list content flattened into user message
|
||||
assert result[1]["role"] == "user"
|
||||
assert "result A" in result[1]["content"]
|
||||
assert "result B" in result[1]["content"]
|
||||
assert isinstance(result[1]["content"], str)
|
||||
|
||||
|
||||
def test_strip_tool_content_merges_consecutive_tool_results() -> None:
|
||||
"""Bedrock requires strict user/assistant alternation. Multiple parallel
|
||||
tool results must be merged into a single user message."""
|
||||
from onyx.llm.multi_llm import _strip_tool_content_from_messages
|
||||
|
||||
messages: list[dict[str, Any]] = [
|
||||
{"role": "user", "content": "weather and news?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "tc_1",
|
||||
"type": "function",
|
||||
"function": {"name": "search_weather", "arguments": "{}"},
|
||||
},
|
||||
{
|
||||
"id": "tc_2",
|
||||
"type": "function",
|
||||
"function": {"name": "search_news", "arguments": "{}"},
|
||||
},
|
||||
],
|
||||
},
|
||||
{"role": "tool", "content": "sunny 72F", "tool_call_id": "tc_1"},
|
||||
{"role": "tool", "content": "headline news", "tool_call_id": "tc_2"},
|
||||
{"role": "assistant", "content": "Here are the results."},
|
||||
]
|
||||
|
||||
result = _strip_tool_content_from_messages(messages)
|
||||
|
||||
# user, assistant (flattened), user (merged tool results), assistant
|
||||
assert len(result) == 4
|
||||
roles = [m["role"] for m in result]
|
||||
assert roles == ["user", "assistant", "user", "assistant"]
|
||||
|
||||
# Both tool results merged into one user message
|
||||
merged = result[2]["content"]
|
||||
assert "tc_1" in merged
|
||||
assert "sunny 72F" in merged
|
||||
assert "tc_2" in merged
|
||||
assert "headline news" in merged
|
||||
|
||||
@@ -11,7 +11,7 @@ dependencies = [
|
||||
"aioboto3==15.1.0",
|
||||
"cohere==5.6.1",
|
||||
"fastapi==0.133.1",
|
||||
"google-cloud-aiplatform==1.121.0",
|
||||
"google-cloud-aiplatform==1.133.0",
|
||||
"google-genai==1.52.0",
|
||||
"litellm==1.81.6",
|
||||
"openai==2.14.0",
|
||||
@@ -144,7 +144,7 @@ dev = [
|
||||
"matplotlib==3.10.8",
|
||||
"mypy-extensions==1.0.0",
|
||||
"mypy==1.13.0",
|
||||
"onyx-devtools==0.6.2",
|
||||
"onyx-devtools==0.6.3",
|
||||
"openapi-generator-cli==7.17.0",
|
||||
"pandas-stubs~=2.3.3",
|
||||
"pre-commit==3.2.2",
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -33,11 +34,15 @@ func NewCherryPickCommand() *cobra.Command {
|
||||
opts := &CherryPickOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "cherry-pick <commit-sha> [<commit-sha>...]",
|
||||
Use: "cherry-pick <commit-or-pr> [<commit-or-pr>...]",
|
||||
Aliases: []string{"cp"},
|
||||
Short: "Cherry-pick one or more commits to a release branch",
|
||||
Short: "Cherry-pick one or more commits (or PRs) to a release branch",
|
||||
Long: `Cherry-pick one or more commits to a release branch and create a PR.
|
||||
|
||||
Arguments can be commit SHAs or GitHub PR numbers. A purely numeric argument
|
||||
with fewer than 6 digits is treated as a PR number and resolved to its merge
|
||||
commit automatically.
|
||||
|
||||
This command will:
|
||||
1. Find the nearest stable version tag
|
||||
2. Fetch the corresponding release branch(es)
|
||||
@@ -54,7 +59,8 @@ If a cherry-pick hits a merge conflict, resolve it manually, then run:
|
||||
Example usage:
|
||||
|
||||
$ ods cherry-pick foo123 bar456 --release 2.5 --release 2.6
|
||||
$ ods cp foo123 --release 2.5`,
|
||||
$ ods cp foo123 --release 2.5
|
||||
$ ods cp 1234 --release 2.5 # cherry-pick merge commit of PR #1234`,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
cont, _ := cmd.Flags().GetBool("continue")
|
||||
if cont {
|
||||
@@ -90,11 +96,12 @@ Example usage:
|
||||
func runCherryPick(cmd *cobra.Command, args []string, opts *CherryPickOptions) {
|
||||
git.CheckGitHubCLI()
|
||||
|
||||
commitSHAs := args
|
||||
// Resolve any PR numbers (e.g. "1234") to their merge commit SHAs
|
||||
commitSHAs, labels := resolveArgs(args)
|
||||
if len(commitSHAs) == 1 {
|
||||
log.Debugf("Cherry-picking commit: %s", commitSHAs[0])
|
||||
log.Debugf("Cherry-picking %s (%s)", labels[0], commitSHAs[0])
|
||||
} else {
|
||||
log.Debugf("Cherry-picking %d commits: %s", len(commitSHAs), strings.Join(commitSHAs, ", "))
|
||||
log.Debugf("Cherry-picking %d commits: %s", len(commitSHAs), strings.Join(labels, ", "))
|
||||
}
|
||||
|
||||
if opts.DryRun {
|
||||
@@ -294,6 +301,11 @@ func runCherryPickContinue() {
|
||||
|
||||
log.Infof("Resuming cherry-pick (original branch: %s, releases: %v)", state.OriginalBranch, state.Releases)
|
||||
|
||||
// If a rebase is in progress (REBASE_HEAD exists), it must be resolved first
|
||||
if git.IsRebaseInProgress() {
|
||||
log.Fatal("A git rebase is in progress. Resolve it first:\n To continue: git rebase --continue\n To abort: git rebase --abort\nThen re-run: ods cherry-pick --continue")
|
||||
}
|
||||
|
||||
// If git cherry-pick is still in progress (CHERRY_PICK_HEAD exists), continue it
|
||||
if git.IsCherryPickInProgress() {
|
||||
log.Info("Continuing in-progress cherry-pick...")
|
||||
@@ -327,6 +339,23 @@ func cherryPickToRelease(commitSHAs, commitMessages []string, branchSuffix, vers
|
||||
return "", fmt.Errorf("failed to checkout existing hotfix branch: %w", err)
|
||||
}
|
||||
|
||||
// Only rebase when the branch has no unique commits (pure fast-forward).
|
||||
// If unique commits exist (e.g. after --continue resolved a cherry-pick
|
||||
// conflict), rebasing would re-apply them and risk the same conflicts.
|
||||
remoteRef := fmt.Sprintf("origin/%s", releaseBranch)
|
||||
uniqueCount, err := git.CountUniqueCommits(hotfixBranch, remoteRef)
|
||||
if err != nil {
|
||||
log.Warnf("Could not determine unique commits, skipping rebase: %v", err)
|
||||
} else if uniqueCount == 0 {
|
||||
log.Infof("Rebasing %s onto %s", hotfixBranch, releaseBranch)
|
||||
if err := git.RunCommand("rebase", "--quiet", remoteRef); err != nil {
|
||||
_ = git.RunCommand("rebase", "--abort")
|
||||
return "", fmt.Errorf("failed to rebase hotfix branch onto %s (rebase aborted, re-run to retry): %w", releaseBranch, err)
|
||||
}
|
||||
} else {
|
||||
log.Infof("Branch %s has %d unique commit(s), skipping rebase", hotfixBranch, uniqueCount)
|
||||
}
|
||||
|
||||
// Check which commits need to be cherry-picked
|
||||
commitsToCherry := []string{}
|
||||
for _, sha := range commitSHAs {
|
||||
@@ -364,7 +393,6 @@ func cherryPickToRelease(commitSHAs, commitMessages []string, branchSuffix, vers
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Push the hotfix branch
|
||||
log.Infof("Pushing hotfix branch: %s", hotfixBranch)
|
||||
pushArgs := []string{"push", "-u", "origin", hotfixBranch}
|
||||
if noVerify {
|
||||
@@ -432,6 +460,40 @@ func performCherryPick(commitSHAs []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// isPRNumber returns true if the argument looks like a GitHub PR number
|
||||
// (purely numeric with fewer than 6 digits).
|
||||
func isPRNumber(arg string) bool {
|
||||
if len(arg) == 0 || len(arg) >= 6 {
|
||||
return false
|
||||
}
|
||||
n, err := strconv.Atoi(arg)
|
||||
return err == nil && n > 0
|
||||
}
|
||||
|
||||
// resolveArgs resolves arguments that may be PR numbers into commit SHAs.
|
||||
// Returns the resolved commit SHAs and a display-friendly label for logging
|
||||
// (e.g. "PR #1234" instead of raw SHA).
|
||||
func resolveArgs(args []string) (commitSHAs []string, labels []string) {
|
||||
commitSHAs = make([]string, len(args))
|
||||
labels = make([]string, len(args))
|
||||
for i, arg := range args {
|
||||
if isPRNumber(arg) {
|
||||
log.Infof("Resolving PR #%s to merge commit...", arg)
|
||||
sha, err := git.ResolvePRToMergeCommit(arg)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to resolve PR #%s: %v", arg, err)
|
||||
}
|
||||
log.Infof("PR #%s → %s", arg, sha)
|
||||
commitSHAs[i] = sha
|
||||
labels[i] = fmt.Sprintf("PR #%s", arg)
|
||||
} else {
|
||||
commitSHAs[i] = arg
|
||||
labels[i] = arg
|
||||
}
|
||||
}
|
||||
return commitSHAs, labels
|
||||
}
|
||||
|
||||
// normalizeVersion ensures the version has a 'v' prefix
|
||||
func normalizeVersion(version string) string {
|
||||
if !strings.HasPrefix(version, "v") {
|
||||
|
||||
144
tools/ods/cmd/desktop.go
Normal file
144
tools/ods/cmd/desktop.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/onyx-dot-app/onyx/tools/ods/internal/paths"
|
||||
)
|
||||
|
||||
type desktopPackageJSON struct {
|
||||
Scripts map[string]string `json:"scripts"`
|
||||
}
|
||||
|
||||
// NewDesktopCommand creates a command that runs npm scripts from the desktop directory.
|
||||
func NewDesktopCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "desktop <script> [args...]",
|
||||
Short: "Run desktop/package.json npm scripts",
|
||||
Long: desktopHelpDescription(),
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) > 0 {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return desktopScriptNames(), cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runDesktopScript(args)
|
||||
},
|
||||
}
|
||||
cmd.Flags().SetInterspersed(false)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runDesktopScript(args []string) {
|
||||
desktopDir, err := desktopDir()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to find desktop directory: %v", err)
|
||||
}
|
||||
|
||||
scriptName := args[0]
|
||||
scriptArgs := args[1:]
|
||||
if len(scriptArgs) > 0 && scriptArgs[0] == "--" {
|
||||
scriptArgs = scriptArgs[1:]
|
||||
}
|
||||
|
||||
npmArgs := []string{"run", scriptName}
|
||||
if len(scriptArgs) > 0 {
|
||||
// npm requires "--" to forward flags to the underlying script.
|
||||
npmArgs = append(npmArgs, "--")
|
||||
npmArgs = append(npmArgs, scriptArgs...)
|
||||
}
|
||||
log.Debugf("Running in %s: npm %v", desktopDir, npmArgs)
|
||||
|
||||
desktopCmd := exec.Command("npm", npmArgs...)
|
||||
desktopCmd.Dir = desktopDir
|
||||
desktopCmd.Stdout = os.Stdout
|
||||
desktopCmd.Stderr = os.Stderr
|
||||
desktopCmd.Stdin = os.Stdin
|
||||
|
||||
if err := desktopCmd.Run(); err != nil {
|
||||
// For wrapped commands, preserve the child process's exit code and
|
||||
// avoid duplicating already-printed stderr output.
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
if code := exitErr.ExitCode(); code != -1 {
|
||||
os.Exit(code)
|
||||
}
|
||||
}
|
||||
log.Fatalf("Failed to run npm: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func desktopScriptNames() []string {
|
||||
scripts, err := loadDesktopScripts()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(scripts))
|
||||
for name := range scripts {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
func desktopHelpDescription() string {
|
||||
description := `Run npm scripts from desktop/package.json.
|
||||
|
||||
Examples:
|
||||
ods desktop dev
|
||||
ods desktop build
|
||||
ods desktop build:dmg`
|
||||
|
||||
scripts := desktopScriptNames()
|
||||
if len(scripts) == 0 {
|
||||
return description + "\n\nAvailable scripts: (unable to load)"
|
||||
}
|
||||
|
||||
return description + "\n\nAvailable scripts:\n " + strings.Join(scripts, "\n ")
|
||||
}
|
||||
|
||||
func loadDesktopScripts() (map[string]string, error) {
|
||||
desktopDir, err := desktopDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
packageJSONPath := filepath.Join(desktopDir, "package.json")
|
||||
data, err := os.ReadFile(packageJSONPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read %s: %w", packageJSONPath, err)
|
||||
}
|
||||
|
||||
var pkg desktopPackageJSON
|
||||
if err := json.Unmarshal(data, &pkg); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse %s: %w", packageJSONPath, err)
|
||||
}
|
||||
|
||||
if pkg.Scripts == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return pkg.Scripts, nil
|
||||
}
|
||||
|
||||
func desktopDir() (string, error) {
|
||||
root, err := paths.GitRoot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(root, "desktop"), nil
|
||||
}
|
||||
@@ -50,6 +50,7 @@ func NewRootCommand() *cobra.Command {
|
||||
cmd.AddCommand(NewPullCommand())
|
||||
cmd.AddCommand(NewRunCICommand())
|
||||
cmd.AddCommand(NewScreenshotDiffCommand())
|
||||
cmd.AddCommand(NewDesktopCommand())
|
||||
cmd.AddCommand(NewWebCommand())
|
||||
cmd.AddCommand(NewWhoisCommand())
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
module github.com/onyx-dot-app/onyx/tools/ods
|
||||
|
||||
go 1.24.11
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/pflag v1.0.9
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -173,6 +174,26 @@ func IsCherryPickInProgress() bool {
|
||||
return cmd.Run() == nil
|
||||
}
|
||||
|
||||
// CountUniqueCommits returns the number of commits on branch that are not on upstream.
|
||||
func CountUniqueCommits(branch, upstream string) (int, error) {
|
||||
cmd := exec.Command("git", "rev-list", "--count", fmt.Sprintf("%s..%s", upstream, branch))
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("git rev-list --count failed: %w", err)
|
||||
}
|
||||
count, err := strconv.Atoi(strings.TrimSpace(string(output)))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse commit count: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// IsRebaseInProgress checks if a rebase is currently in progress
|
||||
func IsRebaseInProgress() bool {
|
||||
cmd := exec.Command("git", "rev-parse", "--verify", "--quiet", "REBASE_HEAD")
|
||||
return cmd.Run() == nil
|
||||
}
|
||||
|
||||
// HasStagedChanges checks if there are staged changes in the index
|
||||
func HasStagedChanges() bool {
|
||||
cmd := exec.Command("git", "diff", "--quiet", "--cached")
|
||||
@@ -216,6 +237,23 @@ func IsCommitAppliedOnBranch(commitSHA, branchName string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ResolvePRToMergeCommit resolves a GitHub PR number to its merge commit SHA
|
||||
func ResolvePRToMergeCommit(prNumber string) (string, error) {
|
||||
cmd := exec.Command("gh", "pr", "view", prNumber, "--json", "mergeCommit", "--jq", ".mergeCommit.oid")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return "", fmt.Errorf("gh pr view failed: %w: %s", err, string(exitErr.Stderr))
|
||||
}
|
||||
return "", fmt.Errorf("gh pr view failed: %w", err)
|
||||
}
|
||||
sha := strings.TrimSpace(string(output))
|
||||
if sha == "" || sha == "null" {
|
||||
return "", fmt.Errorf("PR #%s has no merge commit (is it merged?)", prNumber)
|
||||
}
|
||||
return sha, nil
|
||||
}
|
||||
|
||||
// RunCherryPickContinue runs git cherry-pick --continue --no-edit
|
||||
func RunCherryPickContinue() error {
|
||||
return RunCommandVerboseOnError("cherry-pick", "--continue", "--no-edit")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[build-system]
|
||||
requires = ["hatchling", "go-bin~=1.24.11", "manygo"]
|
||||
requires = ["hatchling", "go-bin~=1.26.0", "manygo"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
|
||||
49
uv.lock
generated
49
uv.lock
generated
@@ -453,14 +453,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "authlib"
|
||||
version = "1.6.6"
|
||||
version = "1.6.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/9b/b1661026ff24bc641b76b78c5222d614776b0c085bcfdac9bd15a1cb4b35/authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e", size = 164894, upload-time = "2025-12-12T08:01:41.464Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/49/dc/ed1681bf1339dd6ea1ce56136bad4baabc6f7ad466e375810702b0237047/authlib-1.6.7.tar.gz", hash = "sha256:dbf10100011d1e1b34048c9d120e83f13b35d69a826ae762b93d2fb5aafc337b", size = 164950, upload-time = "2026-02-06T14:04:14.171Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005, upload-time = "2025-12-12T08:01:40.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/00/3ed12264094ec91f534fae429945efbaa9f8c666f3aa7061cc3b2a26a0cd/authlib-1.6.7-py2.py3-none-any.whl", hash = "sha256:c637340d9a02789d2efa1d003a7437d10d3e565237bcb5fcbc6c134c7b95bab0", size = 244115, upload-time = "2026-02-06T14:04:12.141Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -756,12 +756,20 @@ sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/90/543f556fcfcfa270713eef906b6352ab048e1e557afec12925c991dc93c2/caio-0.9.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6956d9e4a27021c8bd6c9677f3a59eb1d820cc32d0343cea7961a03b1371965", size = 36839, upload-time = "2025-12-26T15:21:40.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/3b/36f3e8ec38dafe8de4831decd2e44c69303d2a3892d16ceda42afed44e1b/caio-0.9.25-cp311-cp311-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf84bfa039f25ad91f4f52944452a5f6f405e8afab4d445450978cd6241d1478", size = 80255, upload-time = "2025-12-26T15:22:20.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/ce/65e64867d928e6aff1b4f0e12dba0ef6d5bf412c240dc1df9d421ac10573/caio-0.9.25-cp311-cp311-manylinux_2_34_aarch64.whl", hash = "sha256:ae3d62587332bce600f861a8de6256b1014d6485cfd25d68c15caf1611dd1f7c", size = 80052, upload-time = "2026-03-04T22:08:20.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/90/e278863c47e14ec58309aa2e38a45882fbe67b4cc29ec9bc8f65852d3e45/caio-0.9.25-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:fc220b8533dcf0f238a6b1a4a937f92024c71e7b10b5a2dfc1c73604a25709bc", size = 78273, upload-time = "2026-03-04T22:08:21.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/c4/8a1b580875303500a9c12b9e0af58cb82e47f5bcf888c2457742a138273c/caio-0.9.25-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb", size = 81502, upload-time = "2026-03-04T22:08:22.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/1c/0fe770b8ffc8362c48134d1592d653a81a3d8748d764bec33864db36319d/caio-0.9.25-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69", size = 80200, upload-time = "2026-03-04T22:08:23.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/12/c39ae2a4037cb10ad5eb3578eb4d5f8c1a2575c62bba675f3406b7ef0824/caio-0.9.25-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f", size = 81523, upload-time = "2026-03-04T22:08:25.187Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/59/f8f2e950eb4f1a5a3883e198dca514b9d475415cb6cd7b78b9213a0dd45a/caio-0.9.25-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7", size = 80243, upload-time = "2026-03-04T22:08:26.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/a4/e534cf7d2d0e8d880e25dd61e8d921ffcfe15bd696734589826f5a2df727/caio-0.9.25-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:628a630eb7fb22381dd8e3c8ab7f59e854b9c806639811fc3f4310c6bd711d79", size = 81565, upload-time = "2026-03-04T22:08:27.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/ed/bf81aeac1d290017e5e5ac3e880fd56ee15e50a6d0353986799d1bc5cfd5/caio-0.9.25-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0ba16aa605ccb174665357fc729cf500679c2d94d5f1458a6f0d5ca48f2060a7", size = 80071, upload-time = "2026-03-04T22:08:28.751Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" },
|
||||
]
|
||||
|
||||
@@ -2123,16 +2131,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "google-auth"
|
||||
version = "2.43.0"
|
||||
version = "2.48.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cachetools" },
|
||||
{ name = "cryptography" },
|
||||
{ name = "pyasn1-modules" },
|
||||
{ name = "rsa" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ff/ef/66d14cf0e01b08d2d51ffc3c20410c4e134a1548fc246a6081eae585a4fe/google_auth-2.43.0.tar.gz", hash = "sha256:88228eee5fc21b62a1b5fe773ca15e67778cb07dc8363adcb4a8827b52d81483", size = 296359, upload-time = "2025-11-06T00:13:36.587Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/d1/385110a9ae86d91cc14c5282c61fe9f4dc41c0b9f7d423c6ad77038c4448/google_auth-2.43.0-py2.py3-none-any.whl", hash = "sha256:af628ba6fa493f75c7e9dbe9373d148ca9f4399b5ea29976519e0a3848eddd16", size = 223114, upload-time = "2025-11-06T00:13:35.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2164,7 +2172,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "google-cloud-aiplatform"
|
||||
version = "1.121.0"
|
||||
version = "1.133.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "docstring-parser" },
|
||||
@@ -2178,12 +2186,11 @@ dependencies = [
|
||||
{ name = "proto-plus" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "shapely" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/86/d1bad9a342122f0f5913cd8b7758ab340aac3f579cffb800d294da605a7c/google_cloud_aiplatform-1.121.0.tar.gz", hash = "sha256:65710396238fa461dbea9b2af9ed23f95458d70d9684e75519c7c9c1601ff308", size = 9705200, upload-time = "2025-10-15T20:27:59.262Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d4/be/31ce7fd658ddebafbe5583977ddee536b2bacc491ad10b5a067388aec66f/google_cloud_aiplatform-1.133.0.tar.gz", hash = "sha256:3a6540711956dd178daaab3c2c05db476e46d94ac25912b8cf4f59b00b058ae0", size = 9921309, upload-time = "2026-01-08T22:11:25.079Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/f6/806b39f86f912133a3071ffa9ff99801a12868216069e26c83a48943116b/google_cloud_aiplatform-1.121.0-py2.py3-none-any.whl", hash = "sha256:1e7105dfd17963207e966550c9544264508efdfded29cf4924c5b86ff4a22efd", size = 8067568, upload-time = "2025-10-15T20:27:54.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/5b/ef74ff65aebb74eaba51078e33ddd897247ba0d1197fd5a7953126205519/google_cloud_aiplatform-1.133.0-py2.py3-none-any.whl", hash = "sha256:dfc81228e987ca10d1c32c7204e2131b3c8d6b7c8e0b4e23bf7c56816bc4c566", size = 8184595, upload-time = "2026-01-08T22:11:22.067Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4622,7 +4629,7 @@ requires-dist = [
|
||||
{ name = "google-api-python-client", marker = "extra == 'backend'", specifier = "==2.86.0" },
|
||||
{ name = "google-auth-httplib2", marker = "extra == 'backend'", specifier = "==0.1.0" },
|
||||
{ name = "google-auth-oauthlib", marker = "extra == 'backend'", specifier = "==1.0.0" },
|
||||
{ name = "google-cloud-aiplatform", specifier = "==1.121.0" },
|
||||
{ name = "google-cloud-aiplatform", specifier = "==1.133.0" },
|
||||
{ name = "google-genai", specifier = "==1.52.0" },
|
||||
{ name = "hatchling", marker = "extra == 'dev'", specifier = "==1.28.0" },
|
||||
{ name = "httpcore", marker = "extra == 'backend'", specifier = "==1.0.9" },
|
||||
@@ -4655,7 +4662,7 @@ requires-dist = [
|
||||
{ name = "numpy", marker = "extra == 'model-server'", specifier = "==2.4.1" },
|
||||
{ name = "oauthlib", marker = "extra == 'backend'", specifier = "==3.2.2" },
|
||||
{ name = "office365-rest-python-client", marker = "extra == 'backend'", specifier = "==2.6.2" },
|
||||
{ name = "onyx-devtools", marker = "extra == 'dev'", specifier = "==0.6.2" },
|
||||
{ name = "onyx-devtools", marker = "extra == 'dev'", specifier = "==0.6.3" },
|
||||
{ name = "openai", specifier = "==2.14.0" },
|
||||
{ name = "openapi-generator-cli", marker = "extra == 'dev'", specifier = "==7.17.0" },
|
||||
{ name = "openinference-instrumentation", marker = "extra == 'backend'", specifier = "==0.1.42" },
|
||||
@@ -4760,20 +4767,20 @@ requires-dist = [{ name = "onyx", extras = ["backend", "dev", "ee"], editable =
|
||||
|
||||
[[package]]
|
||||
name = "onyx-devtools"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "fastapi" },
|
||||
{ name = "openapi-generator-cli" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/20/d9f6089616044b0fb6e097cbae82122de24f3acd97820be4868d5c28ee3f/onyx_devtools-0.6.2-py3-none-any.whl", hash = "sha256:e48d14695d39d62ec3247a4c76ea56604bc5fb635af84c4ff3e9628bcc67b4fb", size = 3785941, upload-time = "2026-02-25T22:33:43.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/f5/f754a717f6b011050eb52ef09895cfa2f048f567f4aa3d5e0f773657dea4/onyx_devtools-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:505f9910a04868ab62d99bb483dc37c9f4ad94fa80e6ac0e6a10b86351c31420", size = 3832182, upload-time = "2026-02-25T22:33:43.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/35/6e653398c62078e87ebb0d03dc944df6691d92ca427c92867309d2d803b7/onyx_devtools-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:edec98e3acc0fa22cf9102c2070409ea7bcf99d7ded72bd8cb184ece8171c36a", size = 3576948, upload-time = "2026-02-25T22:33:42.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/97/cff707c5c3d2acd714365b1023f0100676abc99816a29558319e8ef01d5f/onyx_devtools-0.6.2-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:97abab61216866cdccd8c0a7e27af328776083756ce4fb57c4bd723030449e3b", size = 3439359, upload-time = "2026-02-25T22:33:44.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/98/3b768d18e5599178834b966b447075626d224e048d6eb264d89d19abacb4/onyx_devtools-0.6.2-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:681b038ab6f1457409d14b2490782c7a8014fc0f0f1b9cd69bb2b7199f99aef1", size = 3785959, upload-time = "2026-02-25T22:33:44.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/38/9b047f9e61c14ccf22b8f386c7a57da3965f90737453f3a577a97da45cdf/onyx_devtools-0.6.2-py3-none-win_amd64.whl", hash = "sha256:a2063be6be104b50a7538cf0d26c7f7ab9159d53327dd6f3e91db05d793c95f3", size = 3878776, upload-time = "2026-02-25T22:33:45.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/0f/742f644bae84f5f8f7b500094a2f58da3ff8027fc739944622577e2e2850/onyx_devtools-0.6.2-py3-none-win_arm64.whl", hash = "sha256:00fb90a49a15c932b5cacf818b1b4918e5b5c574bde243dc1828b57690dd5046", size = 3501112, upload-time = "2026-02-25T22:33:41.512Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/e2/e7619722c3ccd18eb38100f776fb3dd6b4ae0fbbee09fca5af7c69a279b5/onyx_devtools-0.6.3-py3-none-any.whl", hash = "sha256:d3a5422945d9da12cafc185f64b39f6e727ee4cc92b37427deb7a38f9aad4966", size = 3945381, upload-time = "2026-03-05T20:39:25.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/09/513d2dabedc1e54ad4376830fc9b34a3d9c164bdbcdedfcdbb8b8154dc5a/onyx_devtools-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:efe300e9f3a2e7ae75f88a4f9e0a5c4c471478296cb1615b6a1f03d247582e13", size = 3978761, upload-time = "2026-03-05T20:39:28.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/41/e757602a0de032d74ed01c7ee57f30e57728fb9cd4f922f50d2affda3889/onyx_devtools-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:594066eed3f917cfab5a8c7eac3d4a210df30259f2049f664787749709345e19", size = 3665378, upload-time = "2026-03-05T20:44:22.696Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/1c/c93b65d0b32e202596a2647922a75c7011cb982f899ddfcfd171f792c58f/onyx_devtools-0.6.3-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:384ef66030b55c0fd68b3898782b5b4b868ff3de119569dfc8544e2ce534b98a", size = 3540890, upload-time = "2026-03-05T20:39:28.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/33/760eb656013f7f0cdff24570480d3dc4e52bbd8e6147ea1e8cf6fad7554f/onyx_devtools-0.6.3-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:82e218f3a49f64910c2c4c34d5dc12d1ea1520a27e0b0f6e4c0949ff9abaf0e1", size = 3945396, upload-time = "2026-03-05T20:39:34.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/eb/f54b3675c464df8a51194ff75afc97c2417659e3a209dc46948b47c28860/onyx_devtools-0.6.3-py3-none-win_amd64.whl", hash = "sha256:8af614ae7229290ef2417cb85270184a1e826ed9a3a34658da93851edb36df57", size = 4045936, upload-time = "2026-03-05T20:39:28.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/b8/5bee38e748f3d4b8ec935766224db1bbc1214c91092e5822c080fccd9130/onyx_devtools-0.6.3-py3-none-win_arm64.whl", hash = "sha256:717589db4b42528d33ae96f8006ee6aad3555034dcfee724705b6576be6a6ec4", size = 3608268, upload-time = "2026-03-05T20:39:28.731Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -7,7 +7,7 @@ import SidebarTab from "@/refresh-components/buttons/SidebarTab";
|
||||
import { SvgSliders } from "@opal/icons";
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
import { useAuthType } from "@/lib/hooks";
|
||||
import { AuthType } from "@/lib/constants";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -28,9 +28,12 @@ export default function Layout({ children }: LayoutProps) {
|
||||
<SettingsLayouts.Header icon={SvgSliders} title="Settings" separator />
|
||||
|
||||
<SettingsLayouts.Body>
|
||||
<div className="grid grid-cols-[auto_1fr]">
|
||||
<Section flexDirection="row" alignItems="start" gap={1.5}>
|
||||
{/* Left: Tab Navigation */}
|
||||
<div className="flex flex-col px-2 w-[12.5rem]">
|
||||
<div
|
||||
data-testid="settings-left-tab-navigation"
|
||||
className="flex flex-col px-2 min-w-[12.5rem]"
|
||||
>
|
||||
<SidebarTab
|
||||
href="/app/settings/general"
|
||||
selected={pathname === "/app/settings/general"}
|
||||
@@ -60,8 +63,8 @@ export default function Layout({ children }: LayoutProps) {
|
||||
</div>
|
||||
|
||||
{/* Right: Tab Content */}
|
||||
<div className="px-4">{children}</div>
|
||||
</div>
|
||||
{children}
|
||||
</Section>
|
||||
</SettingsLayouts.Body>
|
||||
</SettingsLayouts.Root>
|
||||
</AppLayouts.Root>
|
||||
|
||||
@@ -68,7 +68,9 @@ export default function CreateProjectModal({
|
||||
<Button prominence="secondary" onClick={() => modal.toggle(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>Create Project</Button>
|
||||
<Button disabled={!projectName.trim()} onClick={handleSubmit}>
|
||||
Create Project
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
|
||||
64
web/src/hooks/useBrowserInfo.ts
Normal file
64
web/src/hooks/useBrowserInfo.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export interface BrowserInfo {
|
||||
isSafari: boolean;
|
||||
isFirefox: boolean;
|
||||
isChrome: boolean;
|
||||
isChromium: boolean;
|
||||
isEdge: boolean;
|
||||
isOpera: boolean;
|
||||
isIOS: boolean;
|
||||
isMac: boolean;
|
||||
isWindows: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_BROWSER_INFO: BrowserInfo = {
|
||||
isSafari: false,
|
||||
isFirefox: false,
|
||||
isChrome: false,
|
||||
isChromium: false,
|
||||
isEdge: false,
|
||||
isOpera: false,
|
||||
isIOS: false,
|
||||
isMac: false,
|
||||
isWindows: false,
|
||||
};
|
||||
|
||||
export default function useBrowserInfo(): BrowserInfo {
|
||||
const [browserInfo, setBrowserInfo] =
|
||||
useState<BrowserInfo>(DEFAULT_BROWSER_INFO);
|
||||
useEffect(() => {
|
||||
const userAgent = window.navigator.userAgent;
|
||||
|
||||
const isEdge = /Edg/i.test(userAgent);
|
||||
const isOpera = /OPR|Opera/i.test(userAgent);
|
||||
const isFirefox = /Firefox|FxiOS/i.test(userAgent);
|
||||
const isChrome = /Chrome|CriOS/i.test(userAgent) && !isEdge && !isOpera;
|
||||
const isChromium = /Chromium/i.test(userAgent) || isChrome;
|
||||
const isSafari =
|
||||
/Safari/i.test(userAgent) &&
|
||||
!isChromium &&
|
||||
!isEdge &&
|
||||
!isOpera &&
|
||||
!isFirefox;
|
||||
const isIOS = /iPhone|iPad|iPod/i.test(userAgent);
|
||||
const isMac = /Macintosh|Mac OS X/i.test(userAgent);
|
||||
const isWindows = /Win/i.test(userAgent);
|
||||
|
||||
setBrowserInfo({
|
||||
isSafari,
|
||||
isFirefox,
|
||||
isChrome,
|
||||
isChromium,
|
||||
isEdge,
|
||||
isOpera,
|
||||
isIOS,
|
||||
isMac,
|
||||
isWindows,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return browserInfo;
|
||||
}
|
||||
@@ -64,6 +64,7 @@ import { AppMode, useAppMode } from "@/providers/AppModeProvider";
|
||||
import useAppFocus from "@/hooks/useAppFocus";
|
||||
import { useQueryController } from "@/providers/QueryControllerProvider";
|
||||
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
|
||||
import useBrowserInfo from "@/hooks/useBrowserInfo";
|
||||
|
||||
/**
|
||||
* App Header Component
|
||||
@@ -527,8 +528,16 @@ function Root({ children, enableBackground }: AppRootProps) {
|
||||
const { hasBackground, appBackgroundUrl } = useAppBackground();
|
||||
const { resolvedTheme } = useTheme();
|
||||
const appFocus = useAppFocus();
|
||||
const { isSafari } = useBrowserInfo();
|
||||
const isLightMode = resolvedTheme === "light";
|
||||
const showBackground = hasBackground && enableBackground;
|
||||
const horizontalBlurMask = `linear-gradient(
|
||||
to right,
|
||||
transparent 0%,
|
||||
black max(0%, calc(50% - 25rem)),
|
||||
black min(100%, calc(50% + 25rem)),
|
||||
transparent 100%
|
||||
)`;
|
||||
|
||||
return (
|
||||
/* NOTE: Some elements, markdown tables in particular, refer to this `@container` in order to
|
||||
@@ -568,25 +577,25 @@ function Root({ children, enableBackground }: AppRootProps) {
|
||||
{showBackground && appFocus.isChat() && (
|
||||
<>
|
||||
<div className="absolute inset-0 backdrop-blur-[1px] pointer-events-none" />
|
||||
<div
|
||||
className="absolute z-0 inset-0 backdrop-blur-md transition-all duration-600 pointer-events-none"
|
||||
style={{
|
||||
maskImage: `linear-gradient(
|
||||
to right,
|
||||
transparent 0%,
|
||||
black max(0%, calc(50% - 25rem)),
|
||||
black min(100%, calc(50% + 25rem)),
|
||||
transparent 100%
|
||||
)`,
|
||||
WebkitMaskImage: `linear-gradient(
|
||||
to right,
|
||||
transparent 0%,
|
||||
black max(0%, calc(50% - 25rem)),
|
||||
black min(100%, calc(50% + 25rem)),
|
||||
transparent 100%
|
||||
)`,
|
||||
}}
|
||||
/>
|
||||
{isSafari ? (
|
||||
<div
|
||||
className="absolute z-0 inset-0 bg-cover bg-center bg-fixed pointer-events-none"
|
||||
style={{
|
||||
backgroundImage: `url(${appBackgroundUrl})`,
|
||||
filter: "blur(16px)",
|
||||
maskImage: horizontalBlurMask,
|
||||
WebkitMaskImage: horizontalBlurMask,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="absolute z-0 inset-0 backdrop-blur-md transition-all duration-600 pointer-events-none"
|
||||
style={{
|
||||
maskImage: horizontalBlurMask,
|
||||
WebkitMaskImage: horizontalBlurMask,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export const widthClassmap: Record<Length, string> = {
|
||||
export const heightClassmap: Record<Length, string> = {
|
||||
auto: "h-auto",
|
||||
fit: "h-fit",
|
||||
full: "h-full",
|
||||
full: "h-full min-h-0",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -515,10 +515,16 @@ const ModalBody = React.forwardRef<HTMLDivElement, ModalBodyProps>(
|
||||
ref={ref}
|
||||
className={cn(
|
||||
twoTone && "bg-background-tint-01",
|
||||
"h-full min-h-0 overflow-y-auto w-full"
|
||||
"flex-auto min-h-0 overflow-y-auto w-full"
|
||||
)}
|
||||
>
|
||||
<Section padding={1} gap={1} alignItems="start" {...props}>
|
||||
<Section
|
||||
height="auto"
|
||||
padding={1}
|
||||
gap={1}
|
||||
alignItems="start"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { usePathname, useRouter } from "next/navigation";
|
||||
import { SvgAlertTriangle, SvgLogOut } from "@opal/icons";
|
||||
import { Content } from "@opal/layouts";
|
||||
import { useCurrentUser } from "@/hooks/useCurrentUser";
|
||||
import { getExtensionContext } from "@/lib/extension/utils";
|
||||
|
||||
export default function AppHealthBanner() {
|
||||
const router = useRouter();
|
||||
@@ -39,7 +40,18 @@ export default function AppHealthBanner() {
|
||||
// Function to handle the "Log in" button click
|
||||
function handleLogin() {
|
||||
setShowLoggedOutModal(false);
|
||||
router.push("/auth/login");
|
||||
const { isExtension } = getExtensionContext();
|
||||
if (isExtension) {
|
||||
// In the Chrome extension, open login in a new tab so OAuth popups
|
||||
// work correctly (the extension iframe has no navigable URL origin).
|
||||
window.open(
|
||||
window.location.origin + "/auth/login",
|
||||
"_blank",
|
||||
"noopener,noreferrer"
|
||||
);
|
||||
} else {
|
||||
router.push("/auth/login");
|
||||
}
|
||||
}
|
||||
|
||||
// Function to set up expiration timeout
|
||||
|
||||
@@ -188,50 +188,42 @@ export default function ShareChatSessionModal({
|
||||
<Section
|
||||
justifyContent="start"
|
||||
alignItems="stretch"
|
||||
gap={1}
|
||||
height="auto"
|
||||
gap={0.12}
|
||||
>
|
||||
<Section
|
||||
justifyContent="start"
|
||||
alignItems="stretch"
|
||||
height="auto"
|
||||
gap={0.12}
|
||||
>
|
||||
<PrivacyOption
|
||||
icon={SvgLock}
|
||||
title="Private"
|
||||
description="Only you have access to this chat."
|
||||
selected={selectedPrivacy === "private"}
|
||||
onClick={() => setSelectedPrivacy("private")}
|
||||
ariaLabel="share-modal-option-private"
|
||||
/>
|
||||
<PrivacyOption
|
||||
icon={SvgUsers}
|
||||
title="Your Organization"
|
||||
description="Anyone in your organization can view this chat."
|
||||
selected={selectedPrivacy === "public"}
|
||||
onClick={() => setSelectedPrivacy("public")}
|
||||
ariaLabel="share-modal-option-public"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{isShared && (
|
||||
<div aria-label="share-modal-link-input">
|
||||
<InputTypeIn
|
||||
readOnly
|
||||
value={shareLink}
|
||||
rightSection={
|
||||
<CopyIconButton
|
||||
getCopyText={() => shareLink}
|
||||
tooltip="Copy link"
|
||||
size="sm"
|
||||
aria-label="share-modal-copy-link"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<PrivacyOption
|
||||
icon={SvgLock}
|
||||
title="Private"
|
||||
description="Only you have access to this chat."
|
||||
selected={selectedPrivacy === "private"}
|
||||
onClick={() => setSelectedPrivacy("private")}
|
||||
ariaLabel="share-modal-option-private"
|
||||
/>
|
||||
<PrivacyOption
|
||||
icon={SvgUsers}
|
||||
title="Your Organization"
|
||||
description="Anyone in your organization can view this chat."
|
||||
selected={selectedPrivacy === "public"}
|
||||
onClick={() => setSelectedPrivacy("public")}
|
||||
ariaLabel="share-modal-option-public"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{isShared && (
|
||||
<InputTypeIn
|
||||
aria-label="share-modal-link-input"
|
||||
readOnly
|
||||
value={shareLink}
|
||||
rightSection={
|
||||
<CopyIconButton
|
||||
getCopyText={() => shareLink}
|
||||
tooltip="Copy link"
|
||||
size="sm"
|
||||
aria-label="share-modal-copy-link"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
{!isShared && (
|
||||
|
||||
@@ -156,10 +156,7 @@ test.describe("Share Chat Session Modal", () => {
|
||||
expect(patchBody).toEqual({ sharing_status: "public" });
|
||||
|
||||
const linkInput = dialog.locator('[aria-label="share-modal-link-input"]');
|
||||
await expect(linkInput).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const inputValue = await linkInput.locator("input").inputValue();
|
||||
expect(inputValue).toContain("/app/shared/");
|
||||
await expect(linkInput).toHaveValue(/\/app\/shared\//, { timeout: 5000 });
|
||||
|
||||
await expect(submitButton).toHaveText("Copy Link");
|
||||
await expect(dialog.getByText("Chat shared")).toBeVisible();
|
||||
|
||||
36
web/tests/e2e/settings/settings_pages.spec.ts
Normal file
36
web/tests/e2e/settings/settings_pages.spec.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { THEMES, setThemeBeforeNavigation } from "@tests/e2e/utils/theme";
|
||||
import { expectScreenshot } from "@tests/e2e/utils/visualRegression";
|
||||
|
||||
test.use({ storageState: "admin_auth.json" });
|
||||
|
||||
for (const theme of THEMES) {
|
||||
test.describe(`Settings pages (${theme} mode)`, () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setThemeBeforeNavigation(page, theme);
|
||||
});
|
||||
|
||||
test("should screenshot each settings tab", async ({ page }) => {
|
||||
await page.goto("/app/settings");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
const nav = page.getByTestId("settings-left-tab-navigation");
|
||||
const tabs = nav.locator("a");
|
||||
const count = await tabs.count();
|
||||
|
||||
expect(count).toBeGreaterThan(0);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const tab = tabs.nth(i);
|
||||
const href = await tab.getAttribute("href");
|
||||
const slug = href ? href.replace("/app/settings/", "") : `tab-${i}`;
|
||||
|
||||
await tab.click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await expectScreenshot(page, {
|
||||
name: `settings-${theme}-${slug}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
11
widget/package-lock.json
generated
11
widget/package-lock.json
generated
@@ -8,7 +8,7 @@
|
||||
"name": "onyx-chat-widget",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"dompurify": "^3.0.0",
|
||||
"dompurify": "^3.3.2",
|
||||
"lit": "^3.1.0",
|
||||
"marked": "^12.0.0"
|
||||
},
|
||||
@@ -860,10 +860,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
|
||||
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
|
||||
"integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"dompurify": "^3.0.0",
|
||||
"dompurify": "^3.3.2",
|
||||
"lit": "^3.1.0",
|
||||
"marked": "^12.0.0"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user