Compare commits

..

4 Commits

Author SHA1 Message Date
Dane Urban
31d0b5e13e . 2026-02-23 13:43:18 -08:00
Dane Urban
b0a1288382 . 2026-02-23 13:42:53 -08:00
Dane Urban
57fa4ac15f Undo script 2026-02-23 13:42:16 -08:00
Dane Urban
58b5db8491 Code interpreter auto deploy 2026-02-22 21:05:54 -08:00
13 changed files with 178 additions and 332 deletions

View File

@@ -1,31 +0,0 @@
"""code interpreter server model
Revision ID: 7cb492013621
Revises: 0bb4558f35df
Create Date: 2026-02-22 18:54:54.007265
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "7cb492013621"
down_revision = "0bb4558f35df"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"code_interpreter_server",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column(
"server_enabled", sa.Boolean, nullable=False, server_default=sa.true()
),
)
def downgrade() -> None:
op.drop_table("code_interpreter_server")

View File

@@ -4978,12 +4978,3 @@ class ScimGroupMapping(Base):
user_group: Mapped[UserGroup] = relationship(
"UserGroup", foreign_keys=[user_group_id]
)
class CodeInterpreterServer(Base):
"""Details about the code interpreter server"""
__tablename__ = "code_interpreter_server"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
server_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)

View File

@@ -3,8 +3,8 @@ set -e
cleanup() {
echo "Error occurred. Cleaning up..."
docker stop onyx_postgres onyx_vespa onyx_redis onyx_minio onyx_code_interpreter 2>/dev/null || true
docker rm onyx_postgres onyx_vespa onyx_redis onyx_minio onyx_code_interpreter 2>/dev/null || true
docker stop onyx_postgres onyx_vespa onyx_redis onyx_minio 2>/dev/null || true
docker rm onyx_postgres onyx_vespa onyx_redis onyx_minio 2>/dev/null || true
}
# Trap errors and output a message, then cleanup
@@ -20,8 +20,8 @@ MINIO_VOLUME=${4:-""} # Default is empty if not provided
# Stop and remove the existing containers
echo "Stopping and removing existing containers..."
docker stop onyx_postgres onyx_vespa onyx_redis onyx_minio onyx_code_interpreter 2>/dev/null || true
docker rm onyx_postgres onyx_vespa onyx_redis onyx_minio onyx_code_interpreter 2>/dev/null || true
docker stop onyx_postgres onyx_vespa onyx_redis onyx_minio 2>/dev/null || true
docker rm onyx_postgres onyx_vespa onyx_redis onyx_minio 2>/dev/null || true
# Start the PostgreSQL container with optional volume
echo "Starting PostgreSQL container..."
@@ -55,10 +55,6 @@ else
docker run --detach --name onyx_minio --publish 9004:9000 --publish 9005:9001 -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin minio/minio server /data --console-address ":9001"
fi
# Start the Code Interpreter container
echo "Starting Code Interpreter container..."
docker run --detach --name onyx_code_interpreter --publish 8000:8000 --user root -v /var/run/docker.sock:/var/run/docker.sock onyxdotapp/code-interpreter:latest bash ./entrypoint.sh code-interpreter-api
# Ensure alembic runs in the correct directory (backend/)
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
PARENT_DIR="$(dirname "$SCRIPT_DIR")"

View File

@@ -243,12 +243,12 @@ USAGE_LIMIT_CHUNKS_INDEXED_PAID = int(
)
# Per-week API calls using API keys or Personal Access Tokens
USAGE_LIMIT_API_CALLS_TRIAL = int(os.environ.get("USAGE_LIMIT_API_CALLS_TRIAL", "0"))
USAGE_LIMIT_API_CALLS_TRIAL = int(os.environ.get("USAGE_LIMIT_API_CALLS_TRIAL", "400"))
USAGE_LIMIT_API_CALLS_PAID = int(os.environ.get("USAGE_LIMIT_API_CALLS_PAID", "40000"))
# Per-week non-streaming API calls (more expensive, so lower limits)
USAGE_LIMIT_NON_STREAMING_CALLS_TRIAL = int(
os.environ.get("USAGE_LIMIT_NON_STREAMING_CALLS_TRIAL", "0")
os.environ.get("USAGE_LIMIT_NON_STREAMING_CALLS_TRIAL", "80")
)
USAGE_LIMIT_NON_STREAMING_CALLS_PAID = int(
os.environ.get("USAGE_LIMIT_NON_STREAMING_CALLS_PAID", "160")

View File

@@ -487,16 +487,7 @@ services:
code-interpreter:
image: onyxdotapp/code-interpreter:${CODE_INTERPRETER_IMAGE_TAG:-latest}
entrypoint: ["/bin/bash", "-c"]
command: >
"
if [ \"$${CODE_INTERPRETER_BETA_ENABLED}\" = \"True\" ] || [ \"$${CODE_INTERPRETER_BETA_ENABLED}\" = \"true\" ]; then
exec bash ./entrypoint.sh code-interpreter-api;
else
echo 'Skipping code interpreter';
exec tail -f /dev/null;
fi
"
command: ["bash", "./entrypoint.sh", "code-interpreter-api"]
restart: unless-stopped
env_file:
- path: .env

View File

@@ -69,6 +69,4 @@ services:
inference_model_server:
profiles: ["inference"]
# Code interpreter is not needed in minimal mode.
code-interpreter:
profiles: ["code-interpreter"]
code-interpreter: {}

View File

@@ -315,16 +315,7 @@ services:
code-interpreter:
image: onyxdotapp/code-interpreter:${CODE_INTERPRETER_IMAGE_TAG:-latest}
entrypoint: ["/bin/bash", "-c"]
command: >
"
if [ \"$${CODE_INTERPRETER_BETA_ENABLED}\" = \"True\" ] || [ \"$${CODE_INTERPRETER_BETA_ENABLED}\" = \"true\" ]; then
exec bash ./entrypoint.sh code-interpreter-api;
else
echo 'Skipping code interpreter';
exec tail -f /dev/null;
fi
"
command: ["bash", "./entrypoint.sh", "code-interpreter-api"]
restart: unless-stopped
env_file:
- path: .env

View File

@@ -352,16 +352,7 @@ services:
code-interpreter:
image: onyxdotapp/code-interpreter:${CODE_INTERPRETER_IMAGE_TAG:-latest}
entrypoint: ["/bin/bash", "-c"]
command: >
"
if [ \"$${CODE_INTERPRETER_BETA_ENABLED}\" = \"True\" ] || [ \"$${CODE_INTERPRETER_BETA_ENABLED}\" = \"true\" ]; then
exec bash ./entrypoint.sh code-interpreter-api;
else
echo 'Skipping code interpreter';
exec tail -f /dev/null;
fi
"
command: ["bash", "./entrypoint.sh", "code-interpreter-api"]
restart: unless-stopped
env_file:
- path: .env

View File

@@ -527,16 +527,7 @@ services:
code-interpreter:
image: onyxdotapp/code-interpreter:${CODE_INTERPRETER_IMAGE_TAG:-latest}
entrypoint: ["/bin/bash", "-c"]
command: >
"
if [ \"$${CODE_INTERPRETER_BETA_ENABLED}\" = \"True\" ] || [ \"$${CODE_INTERPRETER_BETA_ENABLED}\" = \"true\" ]; then
exec bash ./entrypoint.sh code-interpreter-api;
else
echo 'Skipping code interpreter';
exec tail -f /dev/null;
fi
"
command: ["bash", "./entrypoint.sh", "code-interpreter-api"]
restart: unless-stopped
env_file:
- path: .env

View File

@@ -31,7 +31,6 @@ import Button from "@/refresh-components/buttons/Button";
import CopyIconButton from "@/refresh-components/buttons/CopyIconButton";
import Text from "@/refresh-components/texts/Text";
import { SvgEdit, SvgKey, SvgRefreshCw } from "@opal/icons";
import { useCloudSubscription } from "@/hooks/useCloudSubscription";
function Main() {
const {
@@ -40,8 +39,6 @@ function Main() {
error,
} = useSWR<APIKey[]>("/api/admin/api-key", errorHandlingFetcher);
const canCreateKeys = useCloudSubscription();
const [fullApiKey, setFullApiKey] = useState<string | null>(null);
const [keyIsGenerating, setKeyIsGenerating] = useState(false);
const [showCreateUpdateForm, setShowCreateUpdateForm] = useState(false);
@@ -73,23 +70,12 @@ function Main() {
const introSection = (
<div className="flex flex-col items-start gap-4">
<Text as="p">
API Keys allow you to access Onyx APIs programmatically.
{canCreateKeys
? " Click the button below to generate a new API Key."
: ""}
API Keys allow you to access Onyx APIs programmatically. Click the
button below to generate a new API Key.
</Text>
{canCreateKeys ? (
<CreateButton onClick={() => setShowCreateUpdateForm(true)}>
Create API Key
</CreateButton>
) : (
<div className="flex flex-col gap-2 rounded-lg bg-background-tint-02 p-4">
<Text as="p" text04>
This feature requires an active paid subscription.
</Text>
<Button href="/admin/billing">Upgrade Plan</Button>
</div>
)}
<CreateButton onClick={() => setShowCreateUpdateForm(true)}>
Create API Key
</CreateButton>
</div>
);
@@ -123,7 +109,7 @@ function Main() {
title="New API Key"
icon={SvgKey}
onClose={() => setFullApiKey(null)}
description="Make sure you copy your new API key. You won't be able to see this key again."
description="Make sure you copy your new API key. You wont be able to see this key again."
/>
<Modal.Body>
<Text as="p" className="break-all flex-1">
@@ -138,94 +124,88 @@ function Main() {
{introSection}
{canCreateKeys && (
<>
<Separator />
<Separator />
<Title className="mt-6">Existing API Keys</Title>
<Table className="overflow-visible">
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>API Key</TableHead>
<TableHead>Role</TableHead>
<TableHead>Regenerate</TableHead>
<TableHead>Delete</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredApiKeys.map((apiKey) => (
<TableRow key={apiKey.api_key_id}>
<TableCell>
<Button
internal
onClick={() => handleEdit(apiKey)}
leftIcon={SvgEdit}
>
{apiKey.api_key_name || <i>null</i>}
</Button>
</TableCell>
<TableCell className="max-w-64">
{apiKey.api_key_display}
</TableCell>
<TableCell className="max-w-64">
{apiKey.api_key_role.toUpperCase()}
</TableCell>
<TableCell>
<Button
internal
leftIcon={SvgRefreshCw}
onClick={async () => {
setKeyIsGenerating(true);
const response = await regenerateApiKey(apiKey);
setKeyIsGenerating(false);
if (!response.ok) {
const errorMsg = await response.text();
toast.error(
`Failed to regenerate API Key: ${errorMsg}`
);
return;
}
const newKey = (await response.json()) as APIKey;
setFullApiKey(newKey.api_key);
mutate("/api/admin/api-key");
}}
>
Refresh
</Button>
</TableCell>
<TableCell>
<DeleteButton
onClick={async () => {
const response = await deleteApiKey(apiKey.api_key_id);
if (!response.ok) {
const errorMsg = await response.text();
toast.error(`Failed to delete API Key: ${errorMsg}`);
return;
}
mutate("/api/admin/api-key");
}}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Title className="mt-6">Existing API Keys</Title>
<Table className="overflow-visible">
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>API Key</TableHead>
<TableHead>Role</TableHead>
<TableHead>Regenerate</TableHead>
<TableHead>Delete</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredApiKeys.map((apiKey) => (
<TableRow key={apiKey.api_key_id}>
<TableCell>
<Button
internal
onClick={() => handleEdit(apiKey)}
leftIcon={SvgEdit}
>
{apiKey.api_key_name || <i>null</i>}
</Button>
</TableCell>
<TableCell className="max-w-64">
{apiKey.api_key_display}
</TableCell>
<TableCell className="max-w-64">
{apiKey.api_key_role.toUpperCase()}
</TableCell>
<TableCell>
<Button
internal
leftIcon={SvgRefreshCw}
onClick={async () => {
setKeyIsGenerating(true);
const response = await regenerateApiKey(apiKey);
setKeyIsGenerating(false);
if (!response.ok) {
const errorMsg = await response.text();
toast.error(`Failed to regenerate API Key: ${errorMsg}`);
return;
}
const newKey = (await response.json()) as APIKey;
setFullApiKey(newKey.api_key);
mutate("/api/admin/api-key");
}}
>
Refresh
</Button>
</TableCell>
<TableCell>
<DeleteButton
onClick={async () => {
const response = await deleteApiKey(apiKey.api_key_id);
if (!response.ok) {
const errorMsg = await response.text();
toast.error(`Failed to delete API Key: ${errorMsg}`);
return;
}
mutate("/api/admin/api-key");
}}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{showCreateUpdateForm && (
<OnyxApiKeyForm
onCreateApiKey={(apiKey) => {
setFullApiKey(apiKey.api_key);
}}
onClose={() => {
setShowCreateUpdateForm(false);
setSelectedApiKey(undefined);
mutate("/api/admin/api-key");
}}
apiKey={selectedApiKey}
/>
)}
</>
{showCreateUpdateForm && (
<OnyxApiKeyForm
onCreateApiKey={(apiKey) => {
setFullApiKey(apiKey.api_key);
}}
onClose={() => {
setShowCreateUpdateForm(false);
setSelectedApiKey(undefined);
mutate("/api/admin/api-key");
}}
apiKey={selectedApiKey}
/>
)}
</>
);

View File

@@ -1,25 +0,0 @@
import { NEXT_PUBLIC_CLOUD_ENABLED } from "@/lib/constants";
import { hasPaidSubscription } from "@/lib/billing/interfaces";
import { useBillingInformation } from "@/hooks/useBillingInformation";
/**
* Returns whether the current tenant has an active paid subscription on cloud.
*
* Self-hosted deployments always return true (no billing gate).
* Cloud deployments check billing status via the billing API.
* Returns true while loading to avoid flashing the upgrade prompt.
*/
export function useCloudSubscription(): boolean {
const { data: billingData, isLoading } = useBillingInformation();
if (!NEXT_PUBLIC_CLOUD_ENABLED) {
return true;
}
// Treat loading as subscribed to avoid UI flash
if (isLoading || billingData == null) {
return true;
}
return hasPaidSubscription(billingData);
}

View File

@@ -133,19 +133,6 @@ export function hasActiveSubscription(
return data.status !== null;
}
/**
* Check if the response indicates an active *paid* subscription.
* Returns true only for status === "active" (excludes trialing, past_due, etc.).
*/
export function hasPaidSubscription(
data: BillingInformation | SubscriptionStatus
): data is BillingInformation {
if ("subscribed" in data) {
return false;
}
return data.status === BillingStatus.ACTIVE;
}
/**
* Check if a license is valid and active.
*/

View File

@@ -65,7 +65,6 @@ import { Interactive } from "@opal/core";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import { useSettingsContext } from "@/providers/SettingsProvider";
import SimpleTooltip from "@/refresh-components/SimpleTooltip";
import { useCloudSubscription } from "@/hooks/useCloudSubscription";
interface PAT {
id: number;
@@ -938,8 +937,6 @@ function AccountsAccessSettings() {
useState<CreatedTokenState | null>(null);
const [tokenToDelete, setTokenToDelete] = useState<PAT | null>(null);
const canCreateTokens = useCloudSubscription();
const showPasswordSection = Boolean(user?.password_configured);
const showTokensSection = authType !== null;
@@ -1248,104 +1245,93 @@ function AccountsAccessSettings() {
{showTokensSection && (
<Section gap={0.75}>
<InputLayouts.Title title="Access Tokens" />
{canCreateTokens ? (
<Card padding={0.25}>
<Section gap={0}>
<Section flexDirection="row" padding={0.25} gap={0.5}>
{pats.length === 0 ? (
<Section padding={0.5} alignItems="start">
<Text text03 secondaryBody>
{isLoading
? "Loading tokens..."
: "No access tokens created."}
</Text>
</Section>
) : (
<InputTypeIn
placeholder="Search..."
value={query}
onChange={(e) => setQuery(e.target.value)}
leftSearchIcon
variant="internal"
/>
)}
<CreateButton
onClick={() => setShowCreateModal(true)}
secondary={false}
internal
transient={showCreateModal}
rightIcon
>
New Access Token
</CreateButton>
</Section>
<Card padding={0.25}>
<Section gap={0}>
{/* Header with search/empty state and create button */}
<Section flexDirection="row" padding={0.25} gap={0.5}>
{pats.length === 0 ? (
<Section padding={0.5} alignItems="start">
<Text as="span" text03 secondaryBody>
{isLoading
? "Loading tokens..."
: "No access tokens created."}
</Text>
</Section>
) : (
<InputTypeIn
placeholder="Search..."
value={query}
onChange={(e) => setQuery(e.target.value)}
leftSearchIcon
variant="internal"
/>
)}
<CreateButton
onClick={() => setShowCreateModal(true)}
secondary={false}
internal
transient={showCreateModal}
rightIcon
>
New Access Token
</CreateButton>
</Section>
<Section gap={0.25}>
{filteredPats.map((pat) => {
const now = new Date();
const createdDate = new Date(pat.created_at);
const daysSinceCreation = Math.floor(
(now.getTime() - createdDate.getTime()) /
{/* Token List */}
<Section gap={0.25}>
{filteredPats.map((pat) => {
const now = new Date();
const createdDate = new Date(pat.created_at);
const daysSinceCreation = Math.floor(
(now.getTime() - createdDate.getTime()) /
(1000 * 60 * 60 * 24)
);
let expiryText = "Never expires";
if (pat.expires_at) {
const expiresDate = new Date(pat.expires_at);
const daysUntilExpiry = Math.ceil(
(expiresDate.getTime() - now.getTime()) /
(1000 * 60 * 60 * 24)
);
expiryText = `Expires in ${daysUntilExpiry} day${
daysUntilExpiry === 1 ? "" : "s"
}`;
}
let expiryText = "Never expires";
if (pat.expires_at) {
const expiresDate = new Date(pat.expires_at);
const daysUntilExpiry = Math.ceil(
(expiresDate.getTime() - now.getTime()) /
(1000 * 60 * 60 * 24)
);
expiryText = `Expires in ${daysUntilExpiry} day${
daysUntilExpiry === 1 ? "" : "s"
}`;
}
const middleText = `Created ${daysSinceCreation} day${
daysSinceCreation === 1 ? "" : "s"
} ago - ${expiryText}`;
const middleText = `Created ${daysSinceCreation} day${
daysSinceCreation === 1 ? "" : "s"
} ago - ${expiryText}`;
return (
<Interactive.Container
key={pat.id}
heightVariant="fit"
widthVariant="full"
>
<div className="w-full bg-background-tint-01">
<AttachmentItemLayout
icon={SvgKey}
title={pat.name}
description={pat.token_display}
middleText={middleText}
rightChildren={
<OpalButton
icon={SvgTrash}
onClick={() => setTokenToDelete(pat)}
prominence="tertiary"
size="sm"
aria-label={`Delete token ${pat.name}`}
/>
}
/>
</div>
</Interactive.Container>
);
})}
</Section>
return (
<Interactive.Container
key={pat.id}
heightVariant="fit"
widthVariant="full"
>
<div className="w-full bg-background-tint-01">
<AttachmentItemLayout
icon={SvgKey}
title={pat.name}
description={pat.token_display}
middleText={middleText}
rightChildren={
<OpalButton
icon={SvgTrash}
onClick={() => setTokenToDelete(pat)}
prominence="tertiary"
size="sm"
aria-label={`Delete token ${pat.name}`}
/>
}
/>
</div>
</Interactive.Container>
);
})}
</Section>
</Card>
) : (
<Card>
<Section flexDirection="row" justifyContent="between">
<Text text03 secondaryBody>
Access tokens require an active paid subscription.
</Text>
<Button secondary href="/admin/billing">
Upgrade Plan
</Button>
</Section>
</Card>
)}
</Section>
</Card>
</Section>
)}
</Section>