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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion app/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1513,6 +1513,24 @@ type GenerativeModel implements Node & ModelInterface {
lastUsedAt: DateTime
}

"""A connection to a list of items."""
type GenerativeModelConnection {
"""Pagination data for this connection"""
pageInfo: PageInfo!

"""Contains the nodes in this connection"""
edges: [GenerativeModelEdge!]!
}

"""An edge in a connection."""
type GenerativeModelEdge {
"""A cursor for use in pagination"""
cursor: String!

"""The item at the end of the edge"""
node: GenerativeModel!
}

input GenerativeModelInput {
providerKey: GenerativeProviderKey!
name: String!
Expand Down Expand Up @@ -2373,7 +2391,7 @@ type PromptVersionTagMutationPayload {

type Query {
modelProviders: [GenerativeProvider!]!
generativeModels: [GenerativeModel!]!
generativeModels(first: Int = 50, last: Int, after: String, before: String): GenerativeModelConnection!
playgroundModels(input: ModelsInput = null): [PlaygroundModel!]!
modelInvocationParameters(input: ModelsInput = null): [InvocationParameter!]!
users(first: Int = 50, last: Int, after: String, before: String): UserConnection!
Expand Down
2 changes: 2 additions & 0 deletions app/src/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { RouterProvider } from "react-router/dom";

import { DatasetEvaluatorsPage } from "@phoenix/pages/dataset/evaluators/DatasetEvaluatorsPage";
import { RootLayout } from "@phoenix/pages/RootLayout";
import { settingsPromptsPageLoader } from "@phoenix/pages/settings/prompts/settingsPromptsPageLoader";
import { SettingsAIProvidersPage } from "@phoenix/pages/settings/SettingsAIProvidersPage";
import { settingsAIProvidersPageLoader } from "@phoenix/pages/settings/settingsAIProvidersPageLoader";
import { SettingsAnnotationsPage } from "@phoenix/pages/settings/SettingsAnnotationsPage";
Expand Down Expand Up @@ -389,6 +390,7 @@ const router = createBrowserRouter(
<Route
path="prompts"
element={<SettingsPromptsPage />}
loader={settingsPromptsPageLoader}
handle={{
crumb: () => "Prompts",
}}
Expand Down
20 changes: 6 additions & 14 deletions app/src/components/listbox/ListBox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { forwardRef, Ref } from "react";
import { Ref } from "react";
import {
ListBox as AriaListBox,
ListBoxProps as AriaListBoxProps,
Expand Down Expand Up @@ -64,20 +64,12 @@ const listBoxCSS = css`
}
`;

export interface ListBoxProps<T> extends AriaListBoxProps<T>, StylableProps {}
export interface ListBoxProps<T> extends AriaListBoxProps<T>, StylableProps {
ref?: Ref<HTMLDivElement>;
}

function ListBox<T extends object>(
props: ListBoxProps<T>,
ref: Ref<HTMLDivElement>
) {
const { css: propsCSS, ...restProps } = props;
export function ListBox<T extends object>(props: ListBoxProps<T>) {
const { css: propsCSS, ref, ...restProps } = props;
const mergedCSS = css(listBoxCSS, propsCSS);
return <AriaListBox css={mergedCSS} ref={ref} {...restProps} />;
}

type ListBoxComponent = <T extends object>(
props: ListBoxProps<T> & React.RefAttributes<HTMLDivElement>
) => React.ReactElement<unknown> | null;

const _ListBox = forwardRef(ListBox) as ListBoxComponent;
export { _ListBox as ListBox };
7 changes: 1 addition & 6 deletions app/src/pages/examples/ExamplesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Suspense, useEffect } from "react";
import { Suspense } from "react";
import { usePreloadedQuery } from "react-relay";
import { Outlet, useLoaderData } from "react-router";
import invariant from "tiny-invariant";
Expand All @@ -13,11 +13,6 @@ export function ExamplesPage() {
const loaderData = useLoaderData<typeof examplesLoader>();
invariant(loaderData, "loaderData is required");
const data = usePreloadedQuery(examplesLoaderGql, loaderData);
useEffect(() => {
return () => {
loaderData.dispose();
};
}, [loaderData]);
return (
<ExamplesFilterProvider>
<ExamplesFilterBar />
Expand Down
6 changes: 0 additions & 6 deletions app/src/pages/prompts/PromptsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useEffect } from "react";
import { usePreloadedQuery } from "react-relay";
import { useLoaderData } from "react-router";
import invariant from "tiny-invariant";
Expand All @@ -17,11 +16,6 @@ export function PromptsPage() {
const loaderData = useLoaderData<PromptsLoaderType>();
invariant(loaderData, "loaderData is required");
const data = usePreloadedQuery(promptsLoaderGql, loaderData);
useEffect(() => {
return () => {
loaderData.dispose();
};
}, [loaderData]);

return (
<PromptsFilterProvider>
Expand Down
33 changes: 30 additions & 3 deletions app/src/pages/settings/CloneModelButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Suspense, useState } from "react";
import {
ConnectionHandler,
graphql,
PreloadedQuery,
useMutation,
Expand Down Expand Up @@ -67,12 +68,37 @@ function CloneModelDialogContent({
ModelQuery,
queryReference
);
const connectionId = ConnectionHandler.getConnectionID(
"client:root",
"ModelsTable_generativeModels"
);
const [commitCloneModel, isCommittingCloneModel] =
useMutation<CloneModelButtonMutation>(graphql`
mutation CloneModelButtonMutation($input: CreateModelMutationInput!) {
mutation CloneModelButtonMutation(
$input: CreateModelMutationInput!
$connectionId: ID!
) {
createModel(input: $input) {
query {
...ModelsTable_generativeModels
model
@prependNode(
connections: [$connectionId]
edgeTypeName: "GenerativeModelEdge"
) {
id
name
provider
namePattern
providerKey
startTime
createdAt
updatedAt
lastUsedAt
kind
tokenPrices {
tokenType
kind
costPerMillionTokens
}
}
}
}
Expand Down Expand Up @@ -110,6 +136,7 @@ function CloneModelDialogContent({
})
),
},
connectionId,
},
onCompleted: () => {
onClose();
Expand Down
7 changes: 2 additions & 5 deletions app/src/pages/settings/CreateRetentionPolicy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ import {
/**
* A Wrapper around the RetentionPolicyForm component that is used to create a new retention policy.
*/
export function CreateRetentionPolicy(props: {
onCreate: () => void;
queryId: string;
}) {
export function CreateRetentionPolicy(props: { onCreate: () => void }) {
const notifySuccess = useNotifySuccess();
const notifyError = useNotifyError();
const [submit, isSubmitting] = useMutation<CreateRetentionPolicyMutation>(
Expand Down Expand Up @@ -67,7 +64,7 @@ export function CreateRetentionPolicy(props: {
throw new Error("Invalid retention policy rule");
}
const connectionId = ConnectionHandler.getConnectionID(
props.queryId,
"client:root",
"RetentionPoliciesTable_projectTraceRetentionPolicies"
);
submit({
Expand Down
20 changes: 14 additions & 6 deletions app/src/pages/settings/DeleteModelButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useCallback, useState } from "react";
import { graphql, useMutation } from "react-relay";
import { useRevalidator } from "react-router";
import { ConnectionHandler, graphql, useMutation } from "react-relay";

import {
Button,
Expand Down Expand Up @@ -33,14 +32,23 @@ function DeleteModelDialogContent({
modelName: string;
onClose: () => void;
}) {
const { revalidate } = useRevalidator();
const connectionId = ConnectionHandler.getConnectionID(
"client:root",
"ModelsTable_generativeModels"
);
const [commitDeleteModel, isCommittingDeleteModel] =
useMutation<DeleteModelButtonMutation>(graphql`
mutation DeleteModelButtonMutation($input: DeleteModelMutationInput!) {
mutation DeleteModelButtonMutation(
$input: DeleteModelMutationInput!
$connectionId: ID!
) {
deleteModel(input: $input) {
query {
...ModelsTable_generativeModels
}
model {
id @deleteEdge(connections: [$connectionId])
}
}
}
`);
Expand All @@ -51,14 +59,14 @@ function DeleteModelDialogContent({
commitDeleteModel({
variables: {
input: { id: modelId },
connectionId,
},
onCompleted: () => {
notifySuccess({
title: `Model Deleted`,
message: `The "${modelName}" model has been deleted.`,
});
onClose();
revalidate();
},
onError: (error) => {
const errorMessages = getErrorMessagesFromRelayMutationError(error);
Expand All @@ -75,7 +83,7 @@ function DeleteModelDialogContent({
notifyError,
notifySuccess,
onClose,
revalidate,
connectionId,
]);

return (
Expand Down
82 changes: 46 additions & 36 deletions app/src/pages/settings/GenerativeProvidersCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ export function GenerativeProvidersCard({
}: {
query: GenerativeProvidersCard_data$key;
}) {
const credentials = useCredentialsContext((state) => state);
const data = useFragment<GenerativeProvidersCard_data$key>(
graphql`
fragment GenerativeProvidersCard_data on Query {
Expand Down Expand Up @@ -101,30 +100,14 @@ export function GenerativeProvidersCard({
header: "configuration",
accessorKey: "credentialsSet",
cell: ({ row }) => {
if (!row.original.dependenciesInstalled) {
return <Text color="warning">missing dependencies</Text>;
}

// Check if any credentials are set locally
const credentialRequirements = row.original.credentialRequirements;
if (!isModelProvider(row.original.key)) {
return <Text color="warning">unknown provider key</Text>;
}
const providerCredentials = credentials[row.original.key];
const hasLocalCredentials = credentialRequirements.every(
({ envVarName, isRequired }) => {
const envVarSet = providerCredentials?.[envVarName] !== undefined;
return envVarSet || !isRequired;
}
return (
<ProviderCredentialsStatus
dependenciesInstalled={row.original.dependenciesInstalled}
credentialRequirements={row.original.credentialRequirements}
providerKey={row.original.key}
credentialsSet={row.original.credentialsSet}
/>
);

if (hasLocalCredentials) {
return <Text color="success">local</Text>;
}
if (row.original.credentialsSet) {
return <Text color="success">configured on the server</Text>;
}
return <Text color="text-700">not configured</Text>;
},
},
{
Expand Down Expand Up @@ -154,7 +137,7 @@ export function GenerativeProvidersCard({
},
},
] satisfies ColumnDef<DataRow>[];
}, [credentials]);
}, []);

const table = useReactTable<(typeof tableData)[number]>({
columns,
Expand Down Expand Up @@ -209,6 +192,43 @@ export function GenerativeProvidersCard({
);
}

function ProviderCredentialsStatus({
dependenciesInstalled,
credentialRequirements,
providerKey,
credentialsSet,
}: {
dependenciesInstalled: boolean;
credentialRequirements: GenerativeProvidersCard_data$data["modelProviders"][number]["credentialRequirements"];
providerKey: GenerativeProvidersCard_data$data["modelProviders"][number]["key"];
credentialsSet: boolean;
}) {
const credentials = useCredentialsContext((state) => state);
if (!dependenciesInstalled) {
return <Text color="warning">missing dependencies</Text>;
}

// Check if any credentials are set locally
if (!isModelProvider(providerKey)) {
return <Text color="warning">unknown provider key</Text>;
}
const providerCredentials = credentials[providerKey];
const hasLocalCredentials = credentialRequirements.every(
({ envVarName, isRequired }) => {
const envVarSet = !!providerCredentials?.[envVarName];
return envVarSet || !isRequired;
}
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Credential Check Fails on Empty Strings

The credential check logic now uses !! instead of !== undefined, which causes intentionally empty string environment variables to be treated as "not set." This can incorrectly report providers as "not configured" when an empty string is a valid or desired credential value.

Fix in Cursor Fix in Web


if (hasLocalCredentials) {
return <Text color="success">local</Text>;
}
if (credentialsSet) {
return <Text color="success">configured on the server</Text>;
}
return <Text color="text-700">not configured</Text>;
}

function ProviderCredentialsDialog({
provider,
}: {
Expand All @@ -233,16 +253,6 @@ function ProviderCredentialsDialog({
</View>
<Form>
<ProviderCredentials provider={provider.key} />
<View paddingTop="size-200">
<Flex direction="row" justifyContent="end" gap="size-100">
<Button variant="default" size="S" slot="close">
Cancel
</Button>
<Button variant="primary" size="S" slot="close">
Save Credentials
</Button>
</Flex>
</View>
</Form>
</View>
</DialogContent>
Expand All @@ -268,7 +278,7 @@ function ProviderCredentials({ provider }: { provider: ModelProvider }) {
value,
});
}}
value={credentials?.[credentialConfig.envVarName] ?? undefined}
value={credentials?.[credentialConfig.envVarName] ?? ""}
>
<Label>{credentialConfig.envVarName}</Label>
<CredentialInput />
Expand Down
Loading
Loading