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

## [Unreleased]

## [0.5.1] - 2026-09-21

### Added

- The `grok-4.7` language model (SpaceXAI), served through Vercel AI Gateway
(`VERCEL_MODELS_API_KEY`) or OpenRouter (`OPENROUTER_MODELS_API_KEY`). It
takes over from `grok-4.6` as the featured Grok model; `grok-4.6` stays
available.

### Fixed

- A subscriber closing its `POST /api/v1/channel/{channelId}/subscribe`
connection no longer reports an `AbortError: channel stream aborted` to error
tracking. The subscription ends quietly, as the API documents.
- A fetch action whose request starts with blank lines now runs instead of
failing with `cannot parse initial line`, and an empty request returns
`The fetch request is empty.` to the model without making a call.
- A remote MCP server that does not answer in time (`MCP error -32001: Request
timed out`) is no longer reported to error tracking. The model still receives
the timeout error.
- The built-in `clock10` clock no longer starts on Vercel. An instance frozen
between requests dropped the tick mid-publish and reported `TypeError: fetch
failed` to error tracking about 130 times a day. A serverless deployment
keeps the schedule in its queue backend, as before.

## [0.5.0] - 2026-09-18

### Added
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.5.0",
"version": "0.5.1",
"private": true,
"license": "Apache-2.0",
"packageManager": "pnpm@11.24.0",
Expand Down
106 changes: 102 additions & 4 deletions platform/config/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2237,6 +2237,51 @@ export const openrouterLanguageModels: Record<

// xai

'grok-4.7': {
description: `Grok 4.7 is SpaceXAI's advanced model for coding and professional knowledge work, built to tackle complex, multi-hour tasks with improved self-verification and long-context handling. It strengthens software engineering, document creation, and presentation workflows while maintaining Grok 4.6's speed.`,

provider: 'openrouter',

providerModel: 'x-ai/grok-4.7',

family: 'grok',

features: ['chat', 'functions', 'image', 'reasoning'],

region: 'us',
availableRegions: ['us'],

featured: true,

maxTokens: 500_000,
maxInputTokens: Math.floor(500_000 * MAX_INPUT_TOKENS_RATIO),
maxOutputTokens: Math.ceil(500_000 * MAX_OUTPUT_TOKENS_RATIO),

pricing: {
tokenRatio: 0.2667,
inputTokenRatio: 0.1143,
outputTokenRatio: 0.2667,
inputPrice: 1.6,
outputPrice: 4.8,
},

interactionMaxMessages: DEFAULT_INTERACTION_MAX_MESSAGES,

thresholdStrategy: 'truncate',

visible: true,
deprecated: false,

temperature: DEFAULT_TEMPERATURE,

frequencyPenalty: 0,
presencePenalty: 0,

tags: [],

addedDate: '2026-09-21',
},

'grok-4.6': {
description: `Grok 4.6 builds on Grok 4.5 with a particular focus on long-running agents and more ambitious interactive and visual work. It stays with complex tasks across many steps, whether researching a topic, analyzing information, working across a codebase, or turning an idea into a polished application or work artifact.`,

Expand All @@ -2251,8 +2296,6 @@ export const openrouterLanguageModels: Record<
region: 'us',
availableRegions: ['us'],

featured: true,

maxTokens: 500_000,
maxInputTokens: Math.floor(500_000 * MAX_INPUT_TOKENS_RATIO),
maxOutputTokens: Math.ceil(500_000 * MAX_OUTPUT_TOKENS_RATIO),
Expand Down Expand Up @@ -5781,6 +5824,63 @@ export const vercelLanguageModels: Record<

// xai

'grok-4.7': {
description: `Grok 4.7 is SpaceXAI's advanced model for coding and professional knowledge work, built to tackle complex, multi-hour tasks with improved self-verification and long-context handling. It strengthens software engineering, document creation, and presentation workflows while maintaining Grok 4.6's speed.`,

provider: 'vercel',

providerModel: 'spacexai/grok-4.7',

providerOptions: {
gateway: {
// @note xai is not a ZDR-compliant provider on the Vercel AI
// Gateway and is the only provider serving this model, so we opt it
// out of the platform's forced-ZDR default. With ZDR on, the gateway
// has no ZDR-compliant provider to route to and the request fails
// with no_providers_available. See the 'vercel gateway config' tests
// in lib/model.provider.vercel.utest.js
zeroDataRetention: false,
},
},

family: 'grok',

features: ['chat', 'functions', 'image', 'reasoning'],

region: 'us',
availableRegions: ['us'],

featured: true,

maxTokens: 500_000,
maxInputTokens: Math.floor(500_000 * MAX_INPUT_TOKENS_RATIO),
maxOutputTokens: Math.ceil(500_000 * MAX_OUTPUT_TOKENS_RATIO),

pricing: {
tokenRatio: 0.2,
inputTokenRatio: 0.0857,
outputTokenRatio: 0.2,
inputPrice: 1.2,
outputPrice: 3.6,
},

interactionMaxMessages: DEFAULT_INTERACTION_MAX_MESSAGES,

thresholdStrategy: 'truncate',

visible: true,
deprecated: false,

temperature: DEFAULT_TEMPERATURE,

frequencyPenalty: 0,
presencePenalty: 0,

tags: [],

addedDate: '2026-09-21',
},

'grok-4.6': {
description: `Grok 4.6 builds on Grok 4.5 with a particular focus on long-running agents and more ambitious interactive and visual work. It stays with complex tasks across many steps, whether researching a topic, analyzing information, working across a codebase, or turning an idea into a polished application or work artifact.`,

Expand Down Expand Up @@ -5809,8 +5909,6 @@ export const vercelLanguageModels: Record<
region: 'us',
availableRegions: ['us'],

featured: true,

maxTokens: 500_000,
maxInputTokens: Math.floor(500_000 * MAX_INPUT_TOKENS_RATIO),
maxOutputTokens: Math.ceil(500_000 * MAX_OUTPUT_TOKENS_RATIO),
Expand Down
9 changes: 8 additions & 1 deletion platform/lib/action.exec.fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,8 @@ export function parseRequest(input: string, delim?: string): ParsedRequest {
{
debug(`parsing request as HTTP`, { input, delim })

const request = parseHttpRequest(input, delim) as ParsedRequest
// @note a request line cannot start with whitespace, while yaml above depends on it
const request = parseHttpRequest(input.trimStart(), delim) as ParsedRequest

return request
}
Expand Down Expand Up @@ -588,6 +589,12 @@ export async function executeFetchAction(
'action.exec.fetch.executeFetchAction'
)

if (input.trim() === '') {
return {
error: 'The fetch request is empty.',
}
}

// @todo run through the zod schema declared above

const request = parseRequest(input, '\n')
Expand Down
15 changes: 15 additions & 0 deletions platform/lib/action.exec.fetch.utest.js
Original file line number Diff line number Diff line change
Expand Up @@ -2531,6 +2531,13 @@ options:
expect(result.result).toBeDefined()
})

it('should return an error for a blank request without fetching', async () => {
const result = await executeFetchAction(' \n\n', {}, mockOptions)

expect(result).toEqual({ error: 'The fetch request is empty.' })
expect(fetch).not.toHaveBeenCalled()
})

it('should handle missing optional context values', async () => {
getContextContact.mockReturnValue(null)
getContextTimezone.mockReturnValue(null)
Expand Down Expand Up @@ -3437,6 +3444,14 @@ describe('parseRequest', () => {
expect(result).toEqual(parseHttpRequest(input))
})

it('should ignore blank lines before the HTTP request line', () => {
const result = parseRequest('\n\nGET /api/users\nAccept: text/plain', '\n')

expect(result).toEqual(
parseHttpRequest('GET /api/users\nAccept: text/plain', '\n')
)
})

it('should parse http urls as requests', () => {
const input = 'http://example.com/api'
const result = parseRequest(input)
Expand Down
12 changes: 10 additions & 2 deletions platform/lib/clock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
// Two limits, both inherited from where this runs. A deployment with several
// instances ticks once per instance, and only a queue that deduplicates across
// processes collapses them - the barebone one does not, and says so. A
// serverless host ends the interval with the instance, so a deployment there
// needs its queue backend to keep the schedule.
// serverless host freezes the instance between requests, so the clock does not
// start there and the deployment needs its queue backend to keep the schedule.

import { TEN_MINUTES_IN_MILLISECONDS } from '@chatbotkit-dev/time'

Expand Down Expand Up @@ -74,6 +74,14 @@ export async function tick(now: number = Date.now()): Promise<void> {
* @returns a function that stops the clock
*/
export function startClock(): () => void {
// @note a serverless instance is frozen between requests, so a publish
// started by the timer dies mid-connection; the queue backend's own schedule
// is the clock there

if (process.env.VERCEL) {
return () => {}
}

debug(`clock started`, { interval: CLOCK_INTERVAL }).log('clock.start')

const timer = setInterval(() => {
Expand Down
22 changes: 22 additions & 0 deletions platform/lib/clock.utest.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,28 @@ describe('startClock', () => {

expect(queue).toHaveBeenCalledTimes(1)
})

// @note a serverless instance is frozen between requests, so a publish
// started by a timer dies mid-connection and is reported as a failure
it('never ticks on a serverless host', async () => {
const original = process.env.VERCEL

process.env.VERCEL = '1'

try {
stop = startClock()

await jest.advanceTimersByTimeAsync(CLOCK_INTERVAL * 3)

expect(queue).not.toHaveBeenCalled()
} finally {
if (original === undefined) {
delete process.env.VERCEL
} else {
process.env.VERCEL = original
}
}
})
})

describe('tick', () => {
Expand Down
17 changes: 17 additions & 0 deletions platform/lib/mcp.error.utest.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FetchError } from '@/lib/fetch'
import { rethrowMcpError } from '@/lib/mcp.error'
import { isUnknownError } from '@/lib/response'

import { StreamableHTTPError } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import { McpError } from '@modelcontextprotocol/sdk/types.js'
Expand Down Expand Up @@ -56,6 +57,22 @@ describe('mcp.error', () => {
}
})

it('should treat an McpError request timeout as an expected error', () => {
expect.assertions(3)

const mcpError = new McpError(-32001, 'Request timed out', {
timeout: 60000,
})

try {
rethrowMcpError(mcpError)
} catch (e) {
expect(e).toBeInstanceOf(FetchError)
expect(e.code).toBe('-32001')
expect(isUnknownError(e)).toBe(false)
}
})

it('should not attach meta when McpError has no data', () => {
const mcpError = new McpError(-32001, 'Request timed out')

Expand Down
5 changes: 3 additions & 2 deletions platform/lib/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,13 @@ import { makeJsonSafe } from '@/lib/struct'
export * from '@chatbotkit-dev/http-codes'

// @note error codes this application treats as expected alongside the HTTP
// ones. They are not HTTP codes: one comes from prisma, the other from the
// channel layer.
// ones. They are not HTTP codes: they come from prisma, the channel layer and
// the MCP client.

export const knownExpectedCodesExtra = [
'P2002', // @note prisma specific for unique constraint violation
'no_message_received_aborted', // @note channel wait timeout - expected behavior when AI takes too long
'-32001', // @note mcp request timeout - the user's remote MCP server did not answer in time
]

/**
Expand Down
2 changes: 1 addition & 1 deletion platform/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
"start": "next start",
"storybook": "storybook dev -p ${STORYBOOK_PORT:-8001}",
"studio": "npx prisma studio --port ${STUDIO_PORT:-8002}",
"test": "pnpm test:unit --coverage",
"test": "NODE_OPTIONS=--max-old-space-size=8192 pnpm test:unit --coverage",
"test:integration": "NODE_ENV=test SKIP_FUNCTION_CACHE=true SKIP_USAGE_RECORDING=true SKIP_LOG_RECORDING=true jest -c jest.itest.config.js --forceExit",
"test:unit": "NODE_ENV=test SKIP_FUNCTION_CACHE=true SKIP_USAGE_RECORDING=true SKIP_LOG_RECORDING=true jest -c jest.utest.config.js --forceExit"
},
Expand Down
Loading
Loading