Skip to content

Commit

Permalink
Models API (#23)
Browse files Browse the repository at this point in the history
* add list

* list models works, passes tests

* add to readme

* update version

* add model create to readme

* add create behavior

* add create model func

* fix test

* notify is param is missing

* hardware list and tests

* encode json

* fix json encode/decode state

* add create model to cheatsheet

* update changelog

* update version

* update version
  • Loading branch information
cbh123 authored Nov 6, 2023
1 parent 6c8e966 commit de66cd4
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 3 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
Nothing to see here, yet!
## [2023-11-06]

- Added `Replicate.Models.create/4` function to create a model for a user or organization with a given name, visibility, and hardware SKU.
- Added `Replicate.Hardware.list/0` function to list available hardware SKUs.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ Install by adding `replicate` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:replicate, "~> 1.1.2"}
{:replicate, "~> 1.2.0"}
]
end
```

## Demo

Want to jump right in to building your own apps with Elixir and Replicate? Check out 🔮 [Conjurer](https://github.com/cbh123/getting-started-with-replicate-elixir/blob/main/README.md), a simple demo app we built with the Elixir client.

<video width="400" controls>
Expand Down Expand Up @@ -253,6 +254,21 @@ iex> {{_, 200, 'OK'}, _headers, body} = resp
iex> File.write!("babadook_watercolor.jpg", body)
```

## Create a model

You can create a model for a user or organization
with a given name, visibility, and hardware SKU:

```elixir
iex> {:ok, model} =
Replicate.Models.create(
owner: "your-username",
name: "my-model",
visibility: "public",
hardware: "gpu-a40-large"
)
```

## Create prediction from deployment

Deployments allow you to control the configuration of a model with a private, fixed API endpoint. You can control the version of the model, the hardware it runs on, and how it scales.
Expand Down
15 changes: 15 additions & 0 deletions cheatsheet.cheatmd
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ You can run a model and get a webhook when it completes, instead of waiting for
Replicate.Predictions.create(version, %{prompt: "a 19th century portrait of a wombat gentleman"}, "https://example.com/webhook", ["completed"])
```

### Create a model

You can create a model for a user or organization
with a given name, visibility, and hardware SKU. To see a list of available hardware SKUs, run `Replicate.Hardware.list/0`.

```elixir
iex> {:ok, model} =
Replicate.Models.create(
owner: "your-username",
name: "my-model",
visibility: "public",
hardware: "gpu-a40-large"
)
```

### List versions of a model

You can list all the versions of a model:
Expand Down
52 changes: 52 additions & 0 deletions lib/hardware.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
defmodule Replicate.Hardware do
@moduledoc """
Documentation for `Hardware`.
"""
@behaviour Replicate.Hardware.Behaviour
@replicate_client Application.compile_env(:replicate, :replicate_client, Replicate.Client)
alias Replicate.Hardware
alias Replicate.Hardware.Hardware

@doc """
Lists all hardware.
Returns [Hardware.t()].
## Examples
```
iex> Replicate.Hardware.list()
[%Hardware{
name: "CPU",
sku: "cpu"
},
%Hardware{
name: "Nvidia T4 GPU",
sku: "gpu-t4"
},
%Hardware{
name: "Nvidia A40 GPU",
sku: "gpu-a40-small"
},
%Hardware{
name: "Nvidia A40 (Large) GPU",
sku: "gpu-a40-large"
}
]
```
"""
def list() do
{:ok, results} = @replicate_client.request(:get, "/v1/hardware")

results
|> Jason.decode!()
|> Enum.map(fn h ->
atom_map = string_to_atom(h)
struct!(Hardware, atom_map)
end)
end

defp string_to_atom(body) do
for {k, v} <- body, into: %{}, do: {String.to_atom(k), v}
end
end
7 changes: 7 additions & 0 deletions lib/hardware/behavior.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Replicate.Hardware.Behaviour do
@moduledoc """
Documentation for the Hardware Behaviour
"""
alias Replicate.Hardware.Hardware
@callback list :: list(Hardware.t())
end
7 changes: 7 additions & 0 deletions lib/hardware/hardware.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Replicate.Hardware.Hardware do
@doc """
Documentation for `Hardware`.
"""
@enforce_keys [:sku, :name]
defstruct [:sku, :name]
end
15 changes: 15 additions & 0 deletions lib/mock_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ defmodule Replicate.MockClient do
"latest_version" => @stub_version2
}

@stub_hardware [
%{"name" => "CPU", "sku" => "cpu"},
%{"name" => "Nvidia T4 GPU", "sku" => "gpu-t4"},
%{"name" => "Nvidia A40 GPU", "sku" => "gpu-a40-small"},
%{"name" => "Nvidia A40 (Large) GPU", "sku" => "gpu-a40-large"}
]

def request(:get, "/v1/predictions") do
{:ok, %{"results" => [@stub_prediction, @stub_prediction2]} |> Jason.encode!()}
end
Expand Down Expand Up @@ -117,10 +124,18 @@ defmodule Replicate.MockClient do
|> Jason.encode!()}
end

def request(:get, "/v1/hardware") do
{:ok, @stub_hardware |> Jason.encode!()}
end

def request(:get, path), do: {:error, "Unexpected path in the mock client: #{path}"}

def request(:post, path), do: request(:post, path, [])

def request(:post, "/v1/models", body) do
{:ok, body}
end

def request(:post, path, _body) do
if Path.basename(path) == "cancel" do
{:ok, %{@stub_prediction | status: "canceled"} |> Jason.encode!()}
Expand Down
66 changes: 66 additions & 0 deletions lib/models.ex
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,72 @@ defmodule Replicate.Models do
end
end

@doc """
Create a model.
Args:
owner: The name of the user or organization that will own the model.
name: The name of the model.
visibility: Whether the model should be public or private.
hardware: The SKU for the hardware used to run the model. Possible values can be found by calling `Replicate.Hardware.list()`.
description: A description of the model.
github_url: A URL for the model's source code on GitHub.
paper_url: A URL for the model's paper.
license_url: A URL for the model's license.
cover_image_url: A URL for the model's cover image.
Returns {:ok, %Replicate.Models.Model{}} or {:error, message}.
## Examples
iex> {:ok, model} = Replicate.Models.create(
...> owner: "replicate",
...> name: "hello-world",
...> visibility: "public",
...> hardware: "gpu-a40-large"
...> )
iex> model.owner
"replicate"
"""
def create(opts) do
with {:ok, owner} <- Keyword.fetch(opts, :owner),
{:ok, name} <- Keyword.fetch(opts, :name),
{:ok, visibility} <- Keyword.fetch(opts, :visibility),
{:ok, hardware} <- Keyword.fetch(opts, :hardware) do
description = Keyword.get(opts, :description, nil)
github_url = Keyword.get(opts, :github_url, nil)
paper_url = Keyword.get(opts, :paper_url, nil)
license_url = Keyword.get(opts, :license_url, nil)
cover_image_url = Keyword.get(opts, :cover_image_url, nil)

body =
%{
"owner" => owner,
"name" => name,
"visibility" => visibility,
"hardware" => hardware,
"description" => description,
"github_url" => github_url,
"paper_url" => paper_url,
"license_url" => license_url,
"cover_image_url" => cover_image_url
}
|> Jason.encode!()

case @replicate_client.request(:post, "/v1/models", body) do
{:ok, result} ->
model = Jason.decode!(result) |> string_to_atom()
{:ok, struct(Model, model)}

{:error, message} ->
{:error, message}
end
else
:error ->
{:error, "A required parameter (owner/name/visiblity/hardware) is missing"}
end
end

defp string_to_atom(body) do
for {k, v} <- body, into: %{}, do: {String.to_atom(k), v}
end
Expand Down
9 changes: 9 additions & 0 deletions lib/models/behaviour.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,13 @@ defmodule Replicate.Models.Behaviour do
next: String.t(),
previous: String.t()
}
@callback create(
owner :: String.t(),
name :: String.t(),
description :: String.t(),
github_url :: String.t(),
paper_url :: String.t(),
license_url :: String.t(),
cover_image_url :: String.t()
) :: {:ok, Replicate.Models.Model.t()} | {:error, String.t()}
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Replicate.MixProject do
def project do
[
app: :replicate,
version: "1.1.2",
version: "1.2.0",
elixir: "~> 1.14",
start_permanent: Mix.env() == :prod,
start_permanent: Mix.env() == :prod,
Expand Down
31 changes: 31 additions & 0 deletions test/replicate_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ defmodule ReplicateTest do
import Mox
alias Replicate.Predictions.Prediction
alias Replicate.Models.Model
alias Replicate.Hardware.Hardware
doctest Replicate
doctest Replicate.Predictions
doctest Replicate.Models
doctest Replicate.Deployments
doctest Replicate.Hardware

# Make sure mocks are verified when the test exits
setup :verify_on_exit!
Expand Down Expand Up @@ -129,4 +131,33 @@ defmodule ReplicateTest do

assert first_batch |> length() == 25
end

test "create a model" do
{:ok, model} =
Replicate.Models.create(
owner: "replicate",
name: "babadook",
visibility: "public",
hardware: "gpu-a40-large"
)

assert model.owner == "replicate"
assert model.name == "babadook"
assert model.visibility == "public"

{:error, message} =
Replicate.Models.create(
owner: "replicate",
name: "babadook"
)

assert message == "A required parameter (owner/name/visiblity/hardware) is missing"
end

test "list hardware" do
hardware = Replicate.Hardware.list()

assert hardware |> length() == 4
assert hardware |> Enum.at(0) == %Hardware{name: "CPU", sku: "cpu"}
end
end

0 comments on commit de66cd4

Please sign in to comment.