Skip to content

Commit

Permalink
Merge branch 'main' into sbosio/ai-models-call
Browse files Browse the repository at this point in the history
  • Loading branch information
sbosio committed Sep 28, 2024
2 parents 7715ffc + 7392833 commit 302a21f
Show file tree
Hide file tree
Showing 14 changed files with 682 additions and 3 deletions.
79 changes: 79 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ USAGE
* [`heroku ai:models:attach MODEL_RESOURCE`](#heroku-aimodelsattach-model_resource)
* [`heroku ai:models:call MODEL_RESOURCE`](#heroku-aimodelscall-model_resource)
* [`heroku ai:models:create MODEL_NAME`](#heroku-aimodelscreate-model_name)
* [`heroku ai:models:destroy MODELRESOURCE`](#heroku-aimodelsdestroy-modelresource)
* [`heroku ai:models:detach MODEL_RESOURCE`](#heroku-aimodelsdetach-model_resource)
* [`heroku ai:models:info [MODELRESOURCE]`](#heroku-aimodelsinfo-modelresource)
* [`heroku ai:models:list`](#heroku-aimodelslist)

## `heroku ai:docs`
Expand Down Expand Up @@ -157,6 +160,82 @@ EXAMPLES

_See code: [dist/commands/ai/models/create.ts](https://github.com/heroku/heroku-cli-plugin-integration/blob/v0.0.0/dist/commands/ai/models/create.ts)_

## `heroku ai:models:destroy MODELRESOURCE`

destroy an existing AI model resource

```
USAGE
$ heroku ai:models:destroy [MODELRESOURCE] -a <value> [-c <value>] [-f] [-r <value>]
ARGUMENTS
MODELRESOURCE The resource ID or alias of the model resource to destroy.
FLAGS
-a, --app=<value> (required) app to run command against
-c, --confirm=<value>
-f, --force allow destruction even if connected to other apps
-r, --remote=<value> git remote of app to use
DESCRIPTION
destroy an existing AI model resource
EXAMPLES
$ heroku ai:models:destroy claude-3-5-sonnet-acute-43973
```

_See code: [dist/commands/ai/models/destroy.ts](https://github.com/heroku/heroku-cli-plugin-integration/blob/v0.0.0/dist/commands/ai/models/destroy.ts)_

## `heroku ai:models:detach MODEL_RESOURCE`

Detach a model resource from an app.

```
USAGE
$ heroku ai:models:detach [MODEL_RESOURCE] -a <value> [-r <value>]
ARGUMENTS
MODEL_RESOURCE The resource ID or alias of the model resource to detach
FLAGS
-a, --app=<value> (required) The name of the Heroku app to detach the model resource from.
-r, --remote=<value> git remote of app to use
DESCRIPTION
Detach a model resource from an app.
EXAMPLES
$ heroku ai:models:detach claude-3-5-sonnet-acute-41518 --app example-app
```

_See code: [dist/commands/ai/models/detach.ts](https://github.com/heroku/heroku-cli-plugin-integration/blob/v0.0.0/dist/commands/ai/models/detach.ts)_

## `heroku ai:models:info [MODELRESOURCE]`

get the current status of all the AI model resources attached to your app or a specific resource

```
USAGE
$ heroku ai:models:info [MODELRESOURCE] -a <value> [-r <value>]
ARGUMENTS
MODELRESOURCE The resource ID or alias of the model resource to check.
FLAGS
-a, --app=<value> (required) app to run command against
-r, --remote=<value> git remote of app to use
DESCRIPTION
get the current status of all the AI model resources attached to your app or a specific resource
EXAMPLES
$ heroku ai:models:info claude-3-5-sonnet-acute-04281 --app example-app
$ heroku ai:models:info --app example-app
```

_See code: [dist/commands/ai/models/info.ts](https://github.com/heroku/heroku-cli-plugin-integration/blob/v0.0.0/dist/commands/ai/models/info.ts)_

## `heroku ai:models:list`

list available AI models to provision access to
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"lint": "eslint . --ext .ts --config .eslintrc.json",
"posttest": "yarn lint",
"test": "nyc mocha --forbid-only",
"test:local": "nyc mocha",
"version": "oclif readme && git add README.md"
}
}
38 changes: 38 additions & 0 deletions src/commands/ai/models/destroy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {Args} from '@oclif/core'
import {flags} from '@heroku-cli/command'
import destroyAddon from '../../../lib/ai/models/destroy_addon'
import confirmCommand from '../../../lib/confirmCommand'
import Command from '../../../lib/base'

export default class Destroy extends Command {
static description = 'destroy an existing AI model resource'

static flags = {
app: flags.app({required: true, description: 'app to run command against'}),
confirm: flags.string({char: 'c'}),
force: flags.boolean({char: 'f', description: 'allow destruction even if connected to other apps'}),
remote: flags.remote({description: 'git remote of app to use'}),
}

static args = {
modelResource: Args.string({required: true, description: 'The resource ID or alias of the model resource to destroy.'}),
}

static examples = [
'$ heroku ai:models:destroy claude-3-5-sonnet-acute-43973',
]

public async run(): Promise<void> {
const {flags, args} = await this.parse(Destroy)
const {app, confirm} = flags
const {modelResource} = args
const force = flags.force || process.env.HEROKU_FORCE === '1'

await this.configureHerokuAIClient(modelResource, app)

const aiAddon = this.addon

await confirmCommand(app, confirm)
await destroyAddon(this.config, aiAddon, force)
}
}
46 changes: 46 additions & 0 deletions src/commands/ai/models/detach.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import color from '@heroku-cli/color'
import {flags} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'
import * as Heroku from '@heroku-cli/schema'
import Command from '../../../lib/base'

export default class Detach extends Command {
static description = 'Detach a model resource from an app.'
static flags = {
app: flags.app({description: 'The name of the Heroku app to detach the model resource from.', required: true}),
remote: flags.remote(),
}

static args = {
model_resource: Args.string({
description: 'The resource ID or alias of the model resource to detach',
required: true,
}),
}

static example = 'heroku ai:models:detach claude-3-5-sonnet-acute-41518 --app example-app'

public async run(): Promise<void> {
const {flags, args} = await this.parse(Detach)
const {app} = flags
const {model_resource: modelResource} = args

await this.configureHerokuAIClient(modelResource, app)

const aiAddon = this.addon

ux.action.start(`Detaching ${color.cyan(aiAddon.name || '')} from ${color.magenta(app)}`)

await this.heroku.delete(`/addon-attachments/${aiAddon.id}`)

ux.action.stop()

ux.action.start(`Unsetting ${color.cyan(aiAddon.name || '')} config vars and restarting ${color.magenta(app)}`)

const {body: releases} = await this.heroku.get<Heroku.Release[]>(`/apps/${app}/releases`, {
partial: true, headers: {Range: 'version ..; max=1, order=desc'},
})

ux.action.stop(`done, v${releases[0]?.version || ''}`)
}
}
105 changes: 105 additions & 0 deletions src/commands/ai/models/info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import color from '@heroku-cli/color'
import {flags} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'
import Command from '../../../lib/base'
import {ModelResource} from '../../../lib/ai/types'
import appAddons from '../../../lib/ai/models/app_addons'
import * as Heroku from '@heroku-cli/schema'

export default class Info extends Command {
static description = 'get the current status of all the AI model resources attached to your app or a specific resource'
static examples = [
'heroku ai:models:info claude-3-5-sonnet-acute-04281 --app example-app',
'heroku ai:models:info --app example-app',
]

static flags = {
app: flags.app({required: true}),
remote: flags.remote(),
}

static args = {
modelResource: Args.string({description: 'The resource ID or alias of the model resource to check.'}),
}

public async run(): Promise<any> {
const {args, flags} = await this.parse(Info)
const {app} = flags
const {modelResource} = args
const synthesizedModels: Array<ModelResource> = []
let listOfProvisionedModels: Array<ModelResource> = []

const modelInfo = async () => {
const modelInfoResponse = await this.herokuAI.get<ModelResource>(`/models/${this.apiModelId}`, {
headers: {authorization: `Bearer ${this.apiKey}`},
})
.catch(error => {
if (error.statusCode === 404) {
ux.warn(`We can’t find a model resource called ${color.yellow(modelResource)}.\nRun ${color.cmd('heroku ai:models:info -a <app>')} to see a list of model resources.`)
} else {
throw error
}
})

return modelInfoResponse
}

const getModelDetails = async (collectedModels: Array<Heroku.AddOn> | string) => {
if (typeof collectedModels === 'string') {
const modelResource = collectedModels
await this.configureHerokuAIClient(modelResource, app)

const {body: currentModelResource} = await modelInfo() || {body: null}
synthesizedModels.push(currentModelResource!)
} else {
for (const addonModel of collectedModels) {
await this.configureHerokuAIClient(addonModel.modelResource, app)

const {body: currentModelResource} = await modelInfo() || {body: null}
synthesizedModels.push(currentModelResource!)
}
}

return synthesizedModels
}

if (modelResource) {
listOfProvisionedModels = await getModelDetails(modelResource)
} else {
const provisionedModelsInfo: Record<string, string | undefined>[] = []
const inferenceRegex = /inference/
const addonsResponse = await appAddons(this.config, app)

for (const addonInfo of addonsResponse as Array<Heroku.AddOn>) {
const addonType = addonInfo.addon_service?.name || ''
const isModelAddon = inferenceRegex.test(addonType)

if (isModelAddon) {
provisionedModelsInfo.push({
addonName: addonInfo.addon_service?.name,
modelResource: addonInfo.name,
modelId: addonInfo.addon_service?.id,
})
}
}

listOfProvisionedModels = await getModelDetails(provisionedModelsInfo)
}

this.displayModelResource(listOfProvisionedModels)
}

displayModelResource(modelResources: ModelResource[]) {
for (const modelResource of modelResources) {
ux.log()
ux.styledHeader(modelResource.model_id)
ux.styledObject({
'Base Model ID': modelResource.model_id,
Ready: modelResource.ready,
'Tokens In': modelResource.tokens_in,
'Tokens Out': modelResource.tokens_out,
'Avg Performance': modelResource.avg_performance,
})
}
}
}
17 changes: 17 additions & 0 deletions src/lib/ai/models/app_addons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {Config} from '@oclif/core'
import {APIClient} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'

export default async function (config: Config, app: string) {
const herokuClient = new APIClient(config)

const {body: response} = await herokuClient.get<Heroku.AddOn>(`/apps/${app}/addons`, {
headers: {'Accept-Expansion': 'plan'},
}).catch(error => {
console.log('ERROR MESSAGE:', error.message)
const error_ = error.body && error.body.message ? new Error(`The add-on was unable to be destroyed: ${error.body.message}.`) : new Error(`The add-on was unable to be destroyed: ${error}.`)
throw error_
})

return response
}
23 changes: 23 additions & 0 deletions src/lib/ai/models/destroy_addon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import color from '@heroku-cli/color'
import {ux} from '@oclif/core'
import {Config} from '@oclif/core'
import {APIClient} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'

export default async function (config: Config, addon: Heroku.AddOn, force = false) {
const addonName = addon.name || ''
const herokuClient = new APIClient(config)

ux.action.start(`Destroying ${color.addon(addonName)} in the background.\nThe app will restart when complete...`)

await herokuClient.delete<Heroku.AddOn>(`/apps/${addon.app?.id}/addons/${addon.id}`, {
headers: {'Accept-Expansion': 'plan'},
body: {force},
}).catch(error => {
ux.action.stop('')
const error_ = error.body && error.body.message ? new Error(`The add-on was unable to be destroyed: ${error.body.message}.`) : new Error(`The add-on was unable to be destroyed: ${error}.`)
throw error_
})

ux.action.stop()
}
3 changes: 2 additions & 1 deletion src/lib/ai/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ export type ModelInfo = {
* Object schema for Model Status endpoint responses.
*/
export type ModelResource = {
plan: ModelName
model_id: ModelName
ready: string
created: string
tokens_in: string
tokens_out?: string
Expand Down
21 changes: 20 additions & 1 deletion src/lib/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ export class NotFound extends Error {
public readonly id = 'not_found'
}

export class AppNotFound extends Error {
constructor(appIdentifier?: string) {
const message = heredoc`
We can’t find the ${color.app(appIdentifier)} app. Check your spelling.
`
super(message)
}

public readonly statusCode = 404
public readonly id = 'not_found'
}

export class AmbiguousError extends Error {
constructor(public readonly matches: string[], addonIdentifier: string, appIdentifier?: string) {
const message = heredoc`
Expand Down Expand Up @@ -173,8 +185,9 @@ export default abstract class extends Command {
}

// 5. If we resolved for an add-on, check that it's a Managed Inference add-on or throw a NotFound error.
if (resolvedAddon && resolvedAddon.addon_service.name !== this.addonServiceSlug)
if (resolvedAddon && resolvedAddon.addon_service.name !== this.addonServiceSlug) {
throw new NotFound(addonIdentifier, appIdentifier)
}

// 6. If we resolved for an add-on but not for an attachment yet, try to resolve the attachment
if (resolvedAddon && !resolvedAttachment) {
Expand Down Expand Up @@ -210,11 +223,17 @@ export default abstract class extends Command {
const attachmentNotFound = attachmentResolverError instanceof HerokuAPIError &&
attachmentResolverError.http.statusCode === 404 &&
attachmentResolverError.body.resource === 'add_on attachment'
const appNotFound = attachmentResolverError instanceof HerokuAPIError &&
attachmentResolverError.http.statusCode === 404 &&
attachmentResolverError.body.resource === 'app'
let error = addonResolverError

if (addonNotFound)
error = attachmentNotFound ? new NotFound(addonIdentifier, appIdentifier) : attachmentResolverError

if (appNotFound)
error = new AppNotFound(appIdentifier)

throw error
}

Expand Down
Loading

0 comments on commit 302a21f

Please sign in to comment.