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
2 changes: 1 addition & 1 deletion AGENT_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.6.67
v0.6.68
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
ARG TOOLS_IMAGE=${OS_VARIANT}:${OS_VERSION}
ARG RUNNER_IMAGE=alpine:3.23.4 # TODO: change back to ${OS_VARIANT}:${OS_VERSION}

FROM node:24.11.1-alpine as node

Check warning on line 8 in Dockerfile

View workflow job for this annotation

GitHub Actions / Test Build Docker image

The 'as' keyword should match the case of the 'from' keyword

FromAsCasing: 'as' and 'FROM' keywords' casing do not match More info: https://docs.docker.com/go/dockerfile/rule/from-as-casing/

WORKDIR /app

Expand All @@ -27,12 +27,12 @@
COPY js/console/ ./console/
COPY js/design-system/ ./design-system/

ARG VITE_PROD_SECRET_KEY

Check warning on line 30 in Dockerfile

View workflow job for this annotation

GitHub Actions / Test Build Docker image

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "VITE_PROD_SECRET_KEY") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ARG VITE_SENTRY_DSN
ARG SENTRY_AUTH_TOKEN

Check warning on line 32 in Dockerfile

View workflow job for this annotation

GitHub Actions / Test Build Docker image

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "SENTRY_AUTH_TOKEN") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
ARG GIT_COMMIT

ENV VITE_PROD_SECRET_KEY=${VITE_PROD_SECRET_KEY} \

Check warning on line 35 in Dockerfile

View workflow job for this annotation

GitHub Actions / Test Build Docker image

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "VITE_PROD_SECRET_KEY") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 35 in Dockerfile

View workflow job for this annotation

GitHub Actions / Test Build Docker image

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "SENTRY_AUTH_TOKEN") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
VITE_GIT_COMMIT=${GIT_COMMIT} \
VITE_SENTRY_DSN=${VITE_SENTRY_DSN} \
SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN}
Expand Down Expand Up @@ -88,10 +88,10 @@

RUN mix do db.certs, agent.chart, sentry.package_source_code, release

FROM alpine:3.21.3 as tools

Check warning on line 91 in Dockerfile

View workflow job for this annotation

GitHub Actions / Test Build Docker image

The 'as' keyword should match the case of the 'from' keyword

FromAsCasing: 'as' and 'FROM' keywords' casing do not match More info: https://docs.docker.com/go/dockerfile/rule/from-as-casing/

ARG TARGETARCH=amd64
ENV CLI_VERSION=v0.12.65
ENV CLI_VERSION=v0.12.66

COPY AGENT_VERSION AGENT_VERSION

Expand Down Expand Up @@ -152,4 +152,4 @@

EXPOSE 4000 6000 4369 50051

CMD mkdir -p /tmp/sqlite; /opt/app/bin/console start

Check warning on line 155 in Dockerfile

View workflow job for this annotation

GitHub Actions / Test Build Docker image

JSON arguments recommended for ENTRYPOINT/CMD to prevent unintended behavior related to OS signals

JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals More info: https://docs.docker.com/go/dockerfile/rule/json-args-recommended/
1 change: 1 addition & 0 deletions charts/ai-proxy/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ config:
# OpenAI model prefixes routed to Bedrock Mantle when authentication is configured.
bedrockMantleModelPrefixes:
- gpt-5.6
- gpt-6

secrets:
token: ~
Expand Down
2 changes: 1 addition & 1 deletion go/ai-proxy/args/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ var (
argsProviderAWSRegion = pflag.String("provider-aws-region", helpers.GetPluralEnv(envProviderAWSRegion, ""), "Provider AWS region used to connect to BedRock API.")
argBedrockMantleKey = pflag.String("bedrock-mantle-key", helpers.GetPluralEnv(envBedrockMantleKey, ""), "Amazon Bedrock Mantle API key. Can be overridden via PLRL_BEDROCK_MANTLE_KEY env var.")
argBedrockMantleAWSRegion = pflag.String("bedrock-mantle-aws-region", helpers.GetPluralEnv(envBedrockMantleAWSRegion, defaultBedrockMantleAWSRegion), "AWS region for Amazon Bedrock Mantle. Defaults to us-east-1.")
argBedrockMantlePrefixes = pflag.StringSlice("bedrock-mantle-model-prefixes", helpers.GetPluralEnvSlice(envBedrockMantlePrefixes, []string{"gpt-5.6"}), "OpenAI model prefixes routed to Amazon Bedrock Mantle.")
argBedrockMantlePrefixes = pflag.StringSlice("bedrock-mantle-model-prefixes", helpers.GetPluralEnvSlice(envBedrockMantlePrefixes, []string{"gpt-5.6", "gpt-6"}), "OpenAI model prefixes routed to Amazon Bedrock Mantle.")
argMantleSigV4 = pflag.Bool("mantle-sigv4", helpers.GetPluralEnvBool(envMantleSigV4, false), "Use AWS SigV4 authentication from the default credentials chain for Amazon Bedrock Mantle. Can be overridden via PLRL_MANTLE_SIGV4 env var.")
argPort = pflag.Int("port", defaultPort, "The port to listen on. Defaults to port 8000.")
argAddress = pflag.IP("address", net.ParseIP(defaultAddress), "The IP address to serve on. Defaults to 0.0.0.0 (all interfaces).")
Expand Down
23 changes: 23 additions & 0 deletions go/ai-proxy/proxy/openai/mantle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,29 @@ func TestRouteToMantle(t *testing.T) {
}
}

func TestRouteToMantleRoutesPrefixedGPT6ModelWithoutRewritingIt(t *testing.T) {
requestBody := `{"model":"openai.gpt-6-terra","input":"test"}`
request := &http.Request{
Body: io.NopCloser(bytes.NewBufferString(requestBody)),
}
proxyRequest := &httputil.ProxyRequest{
In: request,
Out: request.Clone(request.Context()),
}

if !routeToMantle(proxyRequest, []string{"gpt-5.6", "gpt-6"}) {
t.Fatal("expected GPT-6 request to route to Mantle")
}

body, err := io.ReadAll(proxyRequest.Out.Body)
if err != nil {
t.Fatal(err)
}
if got := string(body); got != requestBody {
t.Errorf("body: got %q, want %q", got, requestBody)
}
}

func TestRouteToMantleLeavesOtherModelsUnchanged(t *testing.T) {
request := &http.Request{
Body: io.NopCloser(bytes.NewBufferString(`{"model":"gpt-4.1","input":"test"}`)),
Expand Down
20 changes: 16 additions & 4 deletions js/console/src/components/ai/chatbot/toolCallIcons.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { IconProps, PythonLogoIcon, ToolIcon } from '@pluralsh/design-system'
import { ToolCallKind } from './toolCallDisplay'
import {
IconProps,
McpLogoIcon,
PythonLogoIcon,
TerminalIcon,
ToolIcon,
} from '@pluralsh/design-system'
import { isCmdToolKind, ToolCallKind } from './toolCallDisplay'

/** Icon next to a tool name when it is not a configured workbench tool. */
export function ToolCallKindIcon({
Expand All @@ -18,11 +24,17 @@ export function ToolCallKindIcon({
)
}

const Icon = isCmdToolKind(kind)
? TerminalIcon
: kind === 'mcp_tool_call'
? McpLogoIcon
: ToolIcon

return (
<ToolIcon
<Icon
color="icon-xlight"
size={size}
css={{ flexShrink: 0 }}
style={{ flexShrink: 0 }}
{...props}
/>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import { useTheme } from 'styled-components'

import { StackedText } from 'components/utils/table/StackedText'
import { CaptionP } from 'components/utils/typography/Text'
import {
useDeleteWorkbenchKnowledgeMutation,
useUpdateWorkbenchKnowledgeMutation,
WorkbenchKnowledgeAttributes,
} from 'generated/graphql'
import { fromNow } from 'utils/datetime'
import { isNonNullable } from 'utils/isNonNullable'

Expand All @@ -37,6 +42,18 @@ const DUPLICATE_KNOWLEDGE_NAME_ERROR =
const normalizeKnowledgeName = (name: Nullable<string>) =>
(name ?? '').trim().toLowerCase()

const toErrorMessage = (error: unknown) =>
error instanceof Error ? error.message : String(error)

const knowledgeToAttributes = (
entry: WorkbenchFormKnowledge
): WorkbenchKnowledgeAttributes => ({
name: entry.name,
description: entry.description ?? null,
knowledge: entry.knowledge,
labels: entry.labels,
})

const validateKnowledgeName = ({
draftName,
existingNames,
Expand Down Expand Up @@ -79,6 +96,10 @@ export function KnowledgeSubStep({
const theme = useTheme()
const update = createFormUpdater(setFormState)
const entries = formState.workbenchKnowledge
const [updateKnowledge, { loading: updateLoading }] =
useUpdateWorkbenchKnowledgeMutation()
const [deleteKnowledge] = useDeleteWorkbenchKnowledgeMutation()
const [mutationError, setMutationError] = useState<Nullable<string>>(null)
const existingNames = useMemo(
() => entries.map((entry) => entry.name),
[entries]
Expand All @@ -95,14 +116,23 @@ export function KnowledgeSubStep({

const handleEdit = (id: string) => setEditingId(id)

const handleDelete = (id: string) =>
update((d) => {
d.workbenchKnowledge = (d.workbenchKnowledge ?? []).filter(
(entry) => entry.id !== id
)
})
const handleDelete = async (id: string) => {
setMutationError(null)
try {
await deleteKnowledge({ variables: { id } })
update((d) => {
d.workbenchKnowledge = (d.workbenchKnowledge ?? []).filter(
(entry) => entry.id !== id
)
})
} catch (error) {
setMutationError(toErrorMessage(error))
}
}

const handleSave = (draft: WorkbenchFormKnowledge): Nullable<string> => {
const handleSave = async (
draft: WorkbenchFormKnowledge
): Promise<Nullable<string>> => {
const canSave = !!draft.knowledge.trim() && !!draft.name.trim()
if (!canSave) return 'Knowledge name and contents are required.'

Expand All @@ -123,12 +153,22 @@ export function KnowledgeSubStep({
.map((label) => label.trim())
.filter(Boolean),
}
update((d) => {
const list = [...(d.workbenchKnowledge ?? [])]
const idx = list.findIndex((entry) => entry.id === draft.id)
if (idx >= 0) list[idx] = normalizedDraft
d.workbenchKnowledge = list
})
try {
await updateKnowledge({
variables: {
id: normalizedDraft.id,
attributes: knowledgeToAttributes(normalizedDraft),
},
})
update((d) => {
const list = [...(d.workbenchKnowledge ?? [])]
const idx = list.findIndex((entry) => entry.id === draft.id)
if (idx >= 0) list[idx] = normalizedDraft
d.workbenchKnowledge = list
})
} catch (error) {
return toErrorMessage(error)
}
setEditingId(null)
return null
}
Expand All @@ -142,6 +182,7 @@ export function KnowledgeSubStep({
existingNames={existingNames}
onSave={handleSave}
onCancel={handleCancel}
loading={updateLoading}
/>
)
}
Expand Down Expand Up @@ -181,11 +222,14 @@ export function KnowledgeSubStep({
entry={entry}
isLast={idx === entries.length - 1}
onEdit={() => handleEdit(entry.id)}
onDelete={() => handleDelete(entry.id)}
onDelete={() => void handleDelete(entry.id)}
/>
))}
</Card>
)}
{!!mutationError && (
<CaptionP $color="text-danger">{mutationError}</CaptionP>
)}
</FormField>
</Flex>
)
Expand Down Expand Up @@ -250,11 +294,13 @@ function KnowledgeForm({
existingNames,
onSave,
onCancel,
loading,
}: {
initialEntry: WorkbenchFormKnowledge
existingNames: Nullable<string>[]
onSave: (entry: WorkbenchFormKnowledge) => Nullable<string>
onSave: (entry: WorkbenchFormKnowledge) => Promise<Nullable<string>>
onCancel: () => void
loading: boolean
}) {
const [draft, setDraft] = useState<WorkbenchFormKnowledge>(initialEntry)
const [labelDraft, setLabelDraft] = useState('')
Expand Down Expand Up @@ -305,11 +351,12 @@ function KnowledgeForm({
</Button>
) : (
<Button
onClick={() => {
const nextError = onSaveRef.current(draftRef.current)
onClick={async () => {
const nextError = await onSaveRef.current(draftRef.current)
setSaveError(nextError)
}}
disabled={!canSave}
loading={loading}
>
Save knowledge
</Button>
Expand All @@ -318,7 +365,7 @@ function KnowledgeForm({
)

return () => setFooterActions(null)
}, [canContinue, canSave, currentStep, setFooterActions])
}, [canContinue, canSave, currentStep, loading, setFooterActions])

useEffect(() => {
setRightContent(
Expand Down
Loading
Loading