Skip to content
Open
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
19 changes: 12 additions & 7 deletions docs/02-User-Guide/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -861,16 +861,18 @@ Updates the system prompt configuration for a project. Requires project membersh
"text": "You are a helpful assistant specialized in marketing.",
"cache_control": { "type": "ephemeral" }
}
]
],
"system_prompt_mode": "replace"
}
```

**Request Body Fields:**

| Field | Type | Description |
| ----------------------- | ------------------------------------------- | --------------------------------------------------------------------------- |
| `system_prompt_enabled` | `boolean` (optional) | Enable or disable system prompt override. Defaults to `false`. |
| `system_prompt` | `SystemContentBlock[]` or `null` (optional) | Array of content blocks to use as the system prompt, or `null` to clear it. |
| Field | Type | Description |
| ----------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `system_prompt_enabled` | `boolean` (optional) | Enable or disable system prompt override. Defaults to `false`. |
| `system_prompt` | `SystemContentBlock[]` or `null` (optional) | Array of content blocks to use as the system prompt, or `null` to clear it. |
| `system_prompt_mode` | `"replace"` or `"prepend"` (optional) | How the project system prompt interacts with client requests. `"replace"` (default) replaces the entire system field. `"prepend"` places the project blocks before the original request blocks. |

**`SystemContentBlock` Schema:**

Expand All @@ -897,14 +899,17 @@ interface SystemContentBlock {
"text": "You are a helpful assistant specialized in marketing.",
"cache_control": { "type": "ephemeral" }
}
]
],
"system_prompt_mode": "replace"
}
}
```

**Behavior:**

- When `system_prompt_enabled` is `true` and a `system_prompt` is configured, the proxy replaces the `system` field of all incoming Claude API requests for that project with the stored system prompt.
- When `system_prompt_enabled` is `true` and a `system_prompt` is configured:
- **Replace mode** (default): The proxy replaces the `system` field of all incoming Claude API requests for that project with the stored system prompt.
- **Prepend mode**: The proxy prepends the stored system prompt blocks before the original request's system prompt blocks. If the original system is a string, it is normalized to an array. If the request has no system prompt, the project blocks are used as-is.
- When `system_prompt_enabled` is `false`, the original `system` field from client requests is passed through unchanged.
- Sending `"system_prompt": null` clears the stored prompt without affecting `system_prompt_enabled`.

Expand Down
10 changes: 8 additions & 2 deletions docs/02-User-Guide/dashboard-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,10 @@ Each project has a settings page accessible from the project detail view. Settin

#### System Prompt Override

The System Prompt section lets project members define a prompt that replaces the `system` field in all incoming requests:
The System Prompt section lets project members define a prompt that interacts with the `system` field in all incoming requests:

- **Enable/disable toggle**: turn the override on or off without losing the saved prompt
- **Mode selector**: choose between **Replace** (replaces the entire system prompt) and **Prepend** (places the project prompt before the original request prompt)
- **JSON editor**: enter the system prompt as a JSON array of content blocks, for example:

```json
Expand All @@ -183,7 +184,12 @@ The System Prompt section lets project members define a prompt that replaces the

- Changes are saved via `PUT /api/projects/:id/system-prompt` and take effect immediately for new requests.

When the override is enabled, the original `system` value sent by clients is silently replaced. When disabled, the client's `system` field is forwarded unchanged.
When the override is enabled:

- **Replace mode**: the original `system` value sent by clients is replaced with the project's configured prompt.
- **Prepend mode**: the project prompt blocks are placed before the original request's system prompt blocks.

When disabled, the client's `system` field is forwarded unchanged.

### Account Management

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,15 @@ Add two columns to the `projects` table and a dedicated override function in the
```sql
ALTER TABLE projects
ADD COLUMN system_prompt_enabled BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN system_prompt JSONB DEFAULT NULL;
ADD COLUMN system_prompt JSONB DEFAULT NULL,
ADD COLUMN system_prompt_mode VARCHAR(10) NOT NULL DEFAULT 'replace';
```

The `system_prompt_mode` column controls how the project system prompt interacts with client requests:

- `'replace'` (default): Replaces the entire `system` field
- `'prepend'`: Prepends the project system prompt blocks before the original request blocks

The `system_prompt` column stores the full Claude API system content blocks array:

```json
Expand All @@ -74,8 +80,9 @@ This ordering ensures conversation tracking integrity β€” the system hash reflec

### Override Behavior

- **Enabled with non-empty array**: Replaces the entire `system` field regardless of original format (string or array)
- **Enabled with null or empty array `[]`**: No override applied (pass-through)
- **Replace mode** (default): Replaces the entire `system` field regardless of original format (string or array)
- **Prepend mode**: Prepends the project's system prompt blocks before the original request blocks. If the original system is a string, it is normalized to a `[{ type: "text", text: "..." }]` array. If the request has no system prompt, the project blocks are used as-is.
- **Enabled with null or empty array `[]`**: No override applied (pass-through), regardless of mode
- **Disabled**: No override applied, prompt data preserved in database for re-enablement
- **Error during lookup**: Gracefully falls back to original request (logged as warning)

Expand Down Expand Up @@ -106,7 +113,7 @@ Server-side validation via `validateSystemPrompt()`:

- No audit trail of system prompt changes (only `updated_at` timestamp)
- No versioning or rollback capability
- System prompt replacement is all-or-nothing (no prepend/append modes)
- ~~System prompt replacement is all-or-nothing (no prepend/append modes)~~ β€” resolved: prepend mode added alongside replace mode

### Risks and Mitigations

Expand Down
5 changes: 5 additions & 0 deletions docs/06-Reference/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- System prompt prepend mode: projects can now choose to prepend their system prompt before the original request prompt instead of replacing it entirely
- New `system_prompt_mode` column (`'replace'` or `'prepend'`, default `'replace'`) on projects table (migration 023)
- Mode toggle in the dashboard project settings page
- API support via `system_prompt_mode` field on `PUT /api/projects/:id/system-prompt`
- In prepend mode, string system prompts are normalized to content block arrays
- Public token usage status page at `/public/token-usage` (no authentication required)
- Shows Anthropic OAuth rate limit utilization (5h and 7d windows) per account
- Compact multi-column layout with progress bars, reset times, and last-checked timestamps
Expand Down
18 changes: 16 additions & 2 deletions packages/shared/src/database/queries/project-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
UpdateProjectRequest,
Credential,
SlackConfig,
SystemPromptMode,
} from '../../types/credentials'
import type { SystemContentBlock } from '../../types/claude.js'
import { toSafeCredential } from './credential-queries-internal'
Expand Down Expand Up @@ -201,6 +202,10 @@ export async function updateProject(
updates.push(`system_prompt = $${paramIndex++}`)
values.push(request.system_prompt ? JSON.stringify(request.system_prompt) : null)
}
if (request.system_prompt_mode !== undefined) {
updates.push(`system_prompt_mode = $${paramIndex++}`)
values.push(request.system_prompt_mode)
}

if (updates.length === 0) {
const train = await getProjectById(pool, id)
Expand Down Expand Up @@ -331,11 +336,19 @@ export async function getProjectStats(
export async function getProjectSystemPrompt(
pool: Pool,
projectId: string
): Promise<{ enabled: boolean; system_prompt: SystemContentBlock[] | null } | null> {
): Promise<{
enabled: boolean
system_prompt: SystemContentBlock[] | null
mode: SystemPromptMode
} | null> {
const result = await pool.query<{
system_prompt_enabled: boolean
system_prompt: SystemContentBlock[] | null
}>(`SELECT system_prompt_enabled, system_prompt FROM projects WHERE project_id = $1`, [projectId])
system_prompt_mode: SystemPromptMode
}>(
`SELECT system_prompt_enabled, system_prompt, system_prompt_mode FROM projects WHERE project_id = $1`,
[projectId]
)

if (result.rows.length === 0) {
return null
Expand All @@ -345,6 +358,7 @@ export async function getProjectSystemPrompt(
return {
enabled: row.system_prompt_enabled,
system_prompt: row.system_prompt,
mode: row.system_prompt_mode,
}
}

Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/types/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { SystemContentBlock } from './claude.js'
* Database models for credential and project management
*/

export type SystemPromptMode = 'replace' | 'prepend'

export type ProviderType = 'anthropic' | 'bedrock'

export interface BaseCredential {
Expand Down Expand Up @@ -68,6 +70,7 @@ export interface Project {
is_private: boolean
system_prompt_enabled: boolean
system_prompt: SystemContentBlock[] | null
system_prompt_mode: SystemPromptMode
created_at: Date
updated_at: Date
}
Expand Down Expand Up @@ -156,6 +159,7 @@ export interface UpdateProjectRequest {
is_private?: boolean
system_prompt_enabled?: boolean
system_prompt?: SystemContentBlock[] | null
system_prompt_mode?: SystemPromptMode
}

export interface CreateApiKeyRequest {
Expand Down
110 changes: 110 additions & 0 deletions scripts/db/migrations/023-add-system-prompt-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env bun

/**
* Migration: Add system_prompt_mode column to projects table
*
* Extends the system prompt override feature to support a "prepend" mode
* in addition to the existing "replace" mode. In prepend mode, the project's
* system prompt blocks are placed before the original request blocks instead
* of replacing them entirely.
*/

import { Pool } from 'pg'

async function up(pool: Pool): Promise<void> {
const client = await pool.connect()

try {
await client.query('BEGIN')

console.log('Adding system_prompt_mode column to projects table...')

// Add system_prompt_mode column with 'replace' as default (preserves existing behavior)
await client.query(`
ALTER TABLE projects ADD COLUMN IF NOT EXISTS system_prompt_mode VARCHAR(10) NOT NULL DEFAULT 'replace'
`)
console.log('βœ“ Added system_prompt_mode column')

// Verify column was added
const result = await client.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'projects'
AND table_schema = 'public'
AND column_name = 'system_prompt_mode'
`)

if (result.rows.length === 0) {
throw new Error('Verification failed: system_prompt_mode column not found in projects table')
}

console.log('βœ“ Verified column exists')

await client.query('COMMIT')
console.log('βœ… system_prompt_mode column added to projects table successfully')
} catch (error) {
await client.query('ROLLBACK')
console.error('❌ Failed to add system_prompt_mode column:', error)
throw error
} finally {
client.release()
}
}

async function down(pool: Pool): Promise<void> {
const client = await pool.connect()

try {
await client.query('BEGIN')

console.log('Removing system_prompt_mode column from projects table...')

await client.query(`
ALTER TABLE projects DROP COLUMN IF EXISTS system_prompt_mode
`)
console.log('βœ“ Dropped system_prompt_mode column')

await client.query('COMMIT')
console.log('βœ… system_prompt_mode column removed from projects table successfully')
} catch (error) {
await client.query('ROLLBACK')
console.error('❌ Failed to remove system_prompt_mode column:', error)
throw error
} finally {
client.release()
}
}

// Main execution
async function main() {
const databaseUrl = process.env.DATABASE_URL
if (!databaseUrl) {
console.error('❌ DATABASE_URL environment variable is required')
process.exit(1)
}

const pool = new Pool({ connectionString: databaseUrl })

try {
const action = process.argv[2] || 'up'

if (action === 'up') {
await up(pool)
} else if (action === 'down') {
await down(pool)
} else {
console.error(`❌ Unknown action: ${action}. Use 'up' or 'down'`)
process.exit(1)
}
} catch (error) {
console.error('❌ Migration failed:', error)
process.exit(1)
} finally {
await pool.end()
}
}

// Run if executed directly
if (import.meta.main) {
main()
}
Loading
Loading