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
116 changes: 116 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,122 @@ here. The release version is defined in the workspace root `package.json`.

## [Unreleased]

## [0.5.0] - 2026-09-18

### Added

- Decision models, a new model class that answers typed questions
(boolean, choice, score) about a state and returns probabilities. The first
model is `jev` (TypeSafe AI). It is served by whichever provider has a key:
the TypeSafe API (`TYPESAFE_MODELS_API_KEY`, new), Vercel AI Gateway
(`VERCEL_MODELS_API_KEY`) or OpenRouter (`OPENROUTER_MODELS_API_KEY`), in
that order of precedence. `GET /api/v1/platform/model/list?type=decision`
lists the class.
- `POST /api/v1/decision/create` answers typed questions with a decision
model. It takes `model`, `state` and `questions`, returns `answers` and
`usage`, and is part of the public API specification. It is metered against
the token limit on the model's input tokens, like the other model classes.
A deployment with no decision provider key answers a 400 that says so.

### Changed

- The code snippets the application shows for the SDKs, the CLI and Terraform
(on the token, bot, dataset, skillset and secret pages, and in generated
Terraform) now use the `token` option, `api_token` and the
`CHATBOTKIT_API_TOKEN` environment variable. They need the SDK and provider
releases that introduce those names; the former names keep working there.
The SDK guide in `docs/sdks.md` also covers `CHATBOTKIT_API_URL` for pointing
the CLI and Terraform at a deployment.

### Fixed

- The image and video create and edit routes failed with an internal error on
a deployment that has no image or video provider key, because an empty model
catalogue accepts any model name. They now answer a 400 that says no such
model is configured. Editing without naming a model also failed when the
preferred edit model (`gpt-image-1`, `grok-imagine-video`) was not served;
it now falls back to the deployment's default model. Deployments that serve
those models are unaffected.
- The chat app showed a generic failure, and reported an unhandled error to
Sentry, when a completion failed after the stream had started, for example
when the account was out of tokens. The failure left the server action
after it had returned, so the framework removed its message and code. The
React SDK stream now sends it as an error chunk, and the app shows the
prompt that matches the code, such as the limits reached one.
- A timeout or protocol error from a user's MCP server while installing its
tools now surfaces as an upstream error with the MCP code, the way a tool
call already did, instead of a raw `McpError`.
- The Call GitHub API ability failed with a JSON syntax error, reported to
Sentry, whenever the endpoint answered with something other than JSON. A
plain text response, such as a job log, is now returned as text and capped
to its last 60000 characters. A binary response, such as the zip of a
workflow run's logs, is refused with a 400 that names the content type and
points the model at the job logs endpoint.
- An HTTP failure from a user's MCP server during a tool call, such as a
403 or a 502 while the server's container restarts, now surfaces with the
matching error code instead of a generic error. Statuses the platform
treats as expected, such as 401, 403, 404 and 429, no longer reach Sentry.
- Creating or updating a dataset record whose text is only nonprintable
characters, such as a zero-width space, now returns a 400. The text passed
the whitespace check but normalization emptied it, so the vector store
refused the record and the request failed with a 500.
- Writing a file larger than about 768 KiB from a shell skillset action
failed with an opaque 413 from the sandbox service and was reported to
Sentry. The service now accepts write bodies up to 4 MiB, and the exec,
write and rw actions reject contents over 3 MB with a message that tells
the model to write the file in smaller parts.
- The `kimi-k2.5` model advertised a context window of 262114 tokens, a
digit transposition of the 262144 the gateway serves. The catalogue test
that compares configured limits against the live gateway now passes.
- An upstream API refusal inside a skillset ability, such as a GitHub 403,
is no longer reported to Sentry. The error left the handler as a
`FetchError` but was serialized with a generic code, because the bundle
holds several copies of the errors module and `instanceof` does not hold
across them; a `SystemError` now carries a brand the serializer recognises
from any copy.
- Compacting a conversation that contains tool activity now includes the
tool calls and results in the summary. The summary input carried only type
and text, so every activity message was reported as an unexpected state and
dropped before summarization.
- Finishing a dataset import whose sitemap or Notion integration was deleted
while the job ran no longer fails the job with a record-not-found error.
- Onboarding no longer fails at the last step with a byte-length error when
the organization name is written in a non-latin script. The value was
clipped by character count only, while the column is byte-bound.
- Minting or using a JWT secret whose value is not a PEM private key answers
400 with a config error instead of a 500.
- Signing in with an email code no longer fails when the address is typed
with a capital letter, as phone keyboards do. The code form sent the address
as typed while the code was issued under the lowercased one, so every such
attempt was refused and consumed the code.
- The widget frame no longer fails to render in Firefox when the host page
blocks third-party storage. Opening the trace broadcast channel threw a
`SecurityError` inside a render effect, which the error boundary reported
as a page error on every load; both broadcast channel hooks now treat a
refused channel as unavailable.
- Initiating an email integration with a missing `email`, `subject` or
`text` answers 400. The fields were optional in the request schema but
required by the queue payload, so an empty body failed at enqueue time
with a 500.
- A widget message whose session token cannot be refreshed, such as an embed
of a deleted widget, no longer surfaces as an unhandled rejection. The
dispatched submit handler rethrew into nothing, so every attempt reached
Sentry even for expected refusals.
- A function handler that fails with an expected code, such as a client
function whose channel wait timed out, is no longer reported to Sentry. The
outcome for the model is unchanged.
- DeepSeek V4 Pro on the Vercel AI Gateway bills at the Alibaba backend rate
the gateway added, so no routing decision charges more than the model
configuration.
- Mistral Large and Mistral Small follow the current Mistral catalogue: both
carry the 262,144-token context Mistral now serves, and Mistral Small bills
at Mistral's current list price instead of the retired one.

### Removed

- Devstral 2 is no longer offered. Mistral retired it from the Vercel AI
Gateway, so the name now resolves to Mistral Large for existing bots.

## [0.4.1] - 2026-09-14

### Fixed
Expand Down
32 changes: 19 additions & 13 deletions docs/sdks.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@ Sign in and open `/tokens` to create a token. The
SDKs send it as a bearer token. The value shown at creation time is the only
copy, so store it where the client will read it.

The Node.js SDK and CLI read `CHATBOTKIT_API_SECRET`, and the Terraform
provider reads `CHATBOTKIT_API_KEY`. The Python and Go SDKs take the token as
a constructor argument; pass it from whatever environment variable you prefer.
The CLI and the Terraform provider read the token from `CHATBOTKIT_API_TOKEN`,
or `CBK_API_TOKEN` for short. The older `CHATBOTKIT_API_SECRET` and
`CHATBOTKIT_API_KEY` names, and their `CBK_` forms, are still read. The
Node.js, Python and Go SDKs take the token as the `token` option; pass it from
whatever environment variable you prefer. The former option names (`secret`,
and `api_key` in Terraform) still work and are deprecated.

## Point an SDK at your deployment

Expand All @@ -45,9 +48,11 @@ replaces it. The SDKs build request paths as `/api/v1/...` and only strip the
`/api` prefix for the hosted API host, so the override is the bare origin of
your deployment, with no `/api` suffix.

None of the SDKs read the base URL from the environment. Set it in code or
provider configuration, sourcing the value from your own configuration if the
same program has to run against both a local deployment and the hosted API.
The SDK libraries do not read the base URL from the environment; set it in
code. The CLI and the Terraform provider read the deployment origin from
`CHATBOTKIT_API_URL` (or `CBK_API_URL`), so one variable points both at the
same deployment. Plain `http` works for local use, and a path prefix on the
origin is preserved, so a deployment served under a sub-path is reachable.

### Node.js

Expand All @@ -59,7 +64,7 @@ npm install @chatbotkit/sdk
import { BotClient } from '@chatbotkit/sdk/bot/index.js'

const bot = new BotClient({
secret: process.env.CHATBOTKIT_API_SECRET,
token: process.env.CHATBOTKIT_API_TOKEN,
baseUrl: 'http://127.0.0.1:8080',
})

Expand All @@ -82,7 +87,7 @@ import os
from chatbotkit import ChatBotKit

cbk = ChatBotKit(
secret=os.environ["CHATBOTKIT_API_SECRET"],
token=os.environ["CHATBOTKIT_API_TOKEN"],
base_url="http://127.0.0.1:8080",
)

Expand All @@ -97,7 +102,7 @@ go get github.com/chatbotkit/go-sdk

```go
client := sdk.New(sdk.Options{
Secret: os.Getenv("CHATBOTKIT_API_SECRET"),
Token: os.Getenv("CHATBOTKIT_API_TOKEN"),
BaseURL: "http://127.0.0.1:8080",
})

Expand All @@ -107,12 +112,13 @@ bots, err := client.Bot.List(ctx, nil)
### Terraform

The provider speaks GraphQL, so its `base_url` is the full GraphQL endpoint
rather than the origin.
rather than the origin. When `base_url` is not set, the provider derives the
endpoint from the origin in `CHATBOTKIT_API_URL`.

```terraform
provider "chatbotkit" {
api_key = var.chatbotkit_api_key # or CHATBOTKIT_API_KEY
base_url = "http://127.0.0.1:8080/api/v1/graphql"
api_token = var.chatbotkit_api_token # or CHATBOTKIT_API_TOKEN
base_url = "http://127.0.0.1:8080/api/v1/graphql"
}
```

Expand Down Expand Up @@ -142,7 +148,7 @@ describe a local instance.
A plain HTTP call confirms the origin and token before involving an SDK:

```bash
curl -H "Authorization: Bearer $CHATBOTKIT_API_SECRET" \
curl -H "Authorization: Bearer $CHATBOTKIT_API_TOKEN" \
http://127.0.0.1:8080/api/v1/bot/list
```

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "platform",
"version": "0.4.1",
"version": "0.5.0",
"private": true,
"license": "Apache-2.0",
"packageManager": "[email protected]",
Expand Down
42 changes: 42 additions & 0 deletions packages/errors/src/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,48 @@ describe('errorToErrorResponse', () => {
})
})

it('should keep the code of a SystemError from another module copy', () => {
const { errorToErrorResponse } = require('./index')

// @note a second bundled copy of this module has its own SystemError
// class, so only the shared brand identifies it
class ForeignSystemError extends Error {
constructor(message, code) {
super(message)

this.code = code

Object.defineProperty(
this,
Symbol.for('@chatbotkit-dev/errors/SystemError'),
{ value: true, enumerable: false }
)
}
}

const result = errorToErrorResponse(
new ForeignSystemError('Upstream refused', 'NOT_AUTHORIZED')
)

expect(result).toEqual({
code: 'NOT_AUTHORIZED',
message: 'Upstream refused',
})
})

it('should not treat a plain error with a code as a SystemError', () => {
const { errorToErrorResponse } = require('./index')

const error = Object.assign(new Error('socket hang up'), {
code: 'ECONNRESET',
})

expect(errorToErrorResponse(error)).toEqual({
code: 'GENERIC_ERROR',
message: 'socket hang up',
})
})

it('should handle string error', () => {
const { errorToErrorResponse } = require('./index')

Expand Down
45 changes: 36 additions & 9 deletions packages/errors/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ export const CONTENT_MODERATION_ERROR_CODE = 'CONTENT_MODERATION'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Thrown = any

// @note a bundle can carry more than one copy of this module (one per
// runtime), each with its own class, so instanceof alone cannot recognise a
// SystemError thrown by another copy - the brand is shared through the global
// symbol registry

const SYSTEM_ERROR_BRAND = Symbol.for('@chatbotkit-dev/errors/SystemError')

export class SystemError extends Error {
public code: string

Expand All @@ -38,9 +45,28 @@ export class SystemError extends Error {

this.code = code
this.data = data

Object.defineProperty(this, SYSTEM_ERROR_BRAND, {
value: true,
enumerable: false,
})
}
}

/**
* Recognises a SystemError from any copy of this module.
*/
export function isSystemError(error: unknown): error is SystemError {
if (error instanceof SystemError) {
return true
}

return (
error instanceof Error &&
(error as unknown as Record<symbol, unknown>)[SYSTEM_ERROR_BRAND] === true
)
}

/**
* Represents an error that is composed of multiple errors.
*/
Expand Down Expand Up @@ -254,7 +280,7 @@ export function errorIn(error: Error, collection: string[]) {
return collection.includes(error.name) || collection.includes(error.message)
}

export function isKnownError(error: Error|string): boolean {
export function isKnownError(error: Error | string): boolean {
if (typeof error === 'string') {
error = new Error(error)
}
Expand Down Expand Up @@ -290,7 +316,7 @@ export function errorToErrorResponse(error: Thrown): {
}

switch (true) {
case error instanceof SystemError: {
case isSystemError(error): {
return { code: error.code, message: error.message.toString() }
}

Expand Down Expand Up @@ -362,7 +388,7 @@ export function errorResponseToError(
*
*/
export function errorToSystemError(error: Thrown, data?: unknown): SystemError {
if (error instanceof SystemError) {
if (isSystemError(error)) {
return error
}

Expand Down Expand Up @@ -454,9 +480,7 @@ const MAX_CAUSE_DEPTH = 5
*/
export function extractCauseChain(
error: Thrown
):
| Array<{ name?: string; message?: string; code?: string }>
| undefined {
): Array<{ name?: string; message?: string; code?: string }> | undefined {
/** @type {Array<{name?: string, message?: string, code?: string}>} */
const chain: {
name: string | undefined
Expand All @@ -483,8 +507,8 @@ export function extractCauseChain(
typeof current.message === 'string'
? current.message
: typeof current === 'string'
? current
: undefined,
? current
: undefined,

code:
current.code !== undefined && current.code !== null
Expand Down Expand Up @@ -671,7 +695,10 @@ export async function captureException(e: Thrown): Promise<void> {
}
}

export async function captureInputError(e: Thrown, data: unknown): Promise<void> {
export async function captureInputError(
e: Thrown,
data: unknown
): Promise<void> {
// eslint-disable-next-line
console.error(e)

Expand Down
1 change: 1 addition & 0 deletions platform/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ VERCEL_MODELS_API_KEY=
# MISTRAL_MODELS_API_KEY=
# GROQ_MODELS_API_KEY=
# DEEPSEEK_MODELS_API_KEY=
# TYPESAFE_MODELS_API_KEY=

#
# ANALYTICS
Expand Down
4 changes: 2 additions & 2 deletions platform/app/apps/(adhoc)/b4d0c8f2/components.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,7 @@ function RequestCodeBlock({ requestSchema, method, path }) {
"import { ChatBotKit } from '@chatbotkit/sdk'",
'',
'const cbk = new ChatBotKit({',
' secret: process.env.CHATBOTKIT_API_KEY!,',
' token: process.env.CHATBOTKIT_API_TOKEN!,',
` baseUrl: ${JSON.stringify(new URL(url).origin)},`,
'})',
'',
Expand Down Expand Up @@ -709,7 +709,7 @@ function RequestCodeBlock({ requestSchema, method, path }) {
')',
'',
'client := sdk.New(sdk.Options{',
' Secret: os.Getenv("CHATBOTKIT_API_KEY"),',
' Token: os.Getenv("CHATBOTKIT_API_TOKEN"),',
` BaseURL: ${JSON.stringify(new URL(url).origin)},`,
'})',
'',
Expand Down
Loading
Loading