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
28 changes: 21 additions & 7 deletions admin/server/ai.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"slices"
"strings"

"github.com/google/jsonschema-go/jsonschema"
Expand Down Expand Up @@ -65,24 +66,37 @@ func (s *Server) Complete(ctx context.Context, req *adminv1.CompleteRequest) (*a
zap.Int("output_messages", len(res.Message.Content)),
zap.Int("input_tokens", res.InputTokens),
zap.Int("cached_input_tokens", res.CachedInputTokens),
zap.Int("cache_write_input_tokens", res.CacheWriteInputTokens),
zap.Int("output_tokens", res.OutputTokens),
observability.ZapCtx(ctx),
)

// Older runtimes don't support reasoning blocks, so remove them unless the client opted in.
responseMessage := res.Message
if !req.IncludeReasoning {
responseMessage = &aiv1.CompletionMessage{
Role: res.Message.Role,
Data: res.Message.Data,
Content: slices.DeleteFunc(slices.Clone(res.Message.Content), func(block *aiv1.ContentBlock) bool {
return block.GetReasoning() != nil
}),
}
}

// Handle response backwards compatibility: if request used old format,
// populate both data and content fields for old runtime compatibility
responseMessage := res.Message
if needsBackwardsCompatibleResponse {
responseMessage = convertContentToData(res.Message)
responseMessage = convertContentToData(responseMessage)
}

// Any tool use response will be passed to the client (the runtime server) for execution.
return &adminv1.CompleteResponse{
Message: responseMessage,
Provider: res.Provider,
InputTokens: uint32(res.InputTokens),
OutputTokens: uint32(res.OutputTokens),
CachedInputTokens: uint32(res.CachedInputTokens),
Message: responseMessage,
Provider: res.Provider,
InputTokens: uint32(res.InputTokens),
OutputTokens: uint32(res.OutputTokens),
CachedInputTokens: uint32(res.CachedInputTokens),
CacheWriteInputTokens: uint32(res.CacheWriteInputTokens),
}, nil
}

Expand Down
51 changes: 51 additions & 0 deletions admin/server/ai_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package server

import (
"context"
"testing"

"github.com/rilldata/rill/admin"
adminv1 "github.com/rilldata/rill/proto/gen/rill/admin/v1"
aiv1 "github.com/rilldata/rill/proto/gen/rill/ai/v1"
"github.com/rilldata/rill/runtime/drivers"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)

func TestCompleteReasoning(t *testing.T) {
// Older runtimes don't support reasoning blocks, so they're removed unless the client opted in.
s := &Server{
logger: zap.NewNop(),
admin: &admin.Service{AI: reasoningAIService{}},
}
for _, includeReasoning := range []bool{false, true} {
res, err := s.Complete(t.Context(), &adminv1.CompleteRequest{
Messages: []*aiv1.CompletionMessage{{Role: "user", Content: []*aiv1.ContentBlock{{BlockType: &aiv1.ContentBlock_Text{Text: "hi"}}}}},
IncludeReasoning: includeReasoning,
})
require.NoError(t, err)
if includeReasoning {
require.Len(t, res.Message.Content, 2)
require.Equal(t, "sig", res.Message.Content[0].GetReasoning().GetSignature())
} else {
require.Len(t, res.Message.Content, 1)
require.Equal(t, "hello", res.Message.Content[0].GetText())
}
}
}

// reasoningAIService is a drivers.AIService that responds with reasoning followed by text.
type reasoningAIService struct{}

func (reasoningAIService) Complete(_ context.Context, _ *drivers.CompleteOptions) (*drivers.CompleteResult, error) {
return &drivers.CompleteResult{
Message: &aiv1.CompletionMessage{
Role: "assistant",
Content: []*aiv1.ContentBlock{
{BlockType: &aiv1.ContentBlock_Reasoning{Reasoning: &aiv1.Reasoning{Signature: "sig"}}},
{BlockType: &aiv1.ContentBlock_Text{Text: "hello"}},
},
},
Provider: "claude",
}, nil
}
2 changes: 2 additions & 0 deletions cli/cmd/admin/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ type Config struct {
AIDriver string `default:"" split_words:"true"`
OpenAIAPIKey string `envconfig:"openai_api_key"`
ClaudeAPIKey string `envconfig:"claude_api_key"`
ClaudePromptCaching bool `envconfig:"claude_prompt_caching"`
GeminiAPIKey string `envconfig:"gemini_api_key"`
ActivitySinkType string `default:"" split_words:"true"`
ActivitySinkKafkaBrokers string `default:"" split_words:"true"`
Expand Down Expand Up @@ -256,6 +257,7 @@ func StartCmd(ch *cmdutil.Helper) *cobra.Command {
logger.Fatal("RILL_ADMIN_CLAUDE_API_KEY is required when AI driver is 'claude'")
}
aiConfig["api_key"] = conf.ClaudeAPIKey
aiConfig["prompt_caching"] = conf.ClaudePromptCaching
case "gemini":
if conf.GeminiAPIKey == "" {
logger.Fatal("RILL_ADMIN_GEMINI_API_KEY is required when AI driver is 'gemini'")
Expand Down
11 changes: 11 additions & 0 deletions docs/docs/developers/build/connectors/services/claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ For details on managing credentials across environments, see [Configure Local Cr

For additional configuration options (model, temperature, token limits, etc.), see the [Claude connector reference](/reference/project-files/connectors#claude).

### Prompt caching

Set `prompt_caching: true` to cache the tools, system prompt, and conversation between requests. AI chat resends these on every step of answering a question, so caching typically lowers cost and latency: cache reads cost a fraction of the input price, while cache writes cost somewhat more than it (see [Anthropic's pricing](https://www.anthropic.com/pricing)). It is disabled by default.

```yaml
type: connector
driver: claude
api_key: "{{ .env.CLAUDE_API_KEY }}"
prompt_caching: true
```

## Deploy to Rill Cloud

Rill requires you to explicitly provide a Claude API key to use the Claude connector. See the [connector reference](/reference/project-files/connectors#claude) for details.
Expand Down
14 changes: 14 additions & 0 deletions docs/docs/developers/build/connectors/services/openai.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ For details on managing credentials across environments, see [Configure Local Cr

For additional configuration options (model, base URL, API type, etc.), see the [OpenAI connector reference](/reference/project-files/connectors#openai).

:::note Reasoning models

Rill sends a `temperature` of `0.1` unless you set `reasoning_effort`. OpenAI's reasoning models reject `temperature` unless reasoning effort is `none`, so if you use a model that reasons by default, set `reasoning_effort` (e.g., `medium`) in the connector:

```yaml
type: connector
driver: openai
api_key: "{{ .env.openai_api_key }}"
model: gpt-5.6
reasoning_effort: medium
```

:::

## Deploy to Rill Cloud

Rill requires you to explicitly provide an OpenAI API key to use the OpenAI connector. See the [connector reference](/reference/project-files/connectors#openai) for details.
Expand Down
10 changes: 7 additions & 3 deletions docs/docs/reference/project-files/connectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,7 @@ _[number]_ - Maximum number of tokens to generate in the completion (default: 81

### `reasoning_effort`

_[string]_ - Constrains effort on reasoning for reasoning models (e.g., 'low', 'medium', 'high')
_[string]_ - Constrains effort on reasoning for reasoning models (e.g., 'low', 'medium', 'high'). Set this when using a model that reasons by default, since Rill otherwise sends a temperature of 0.1, which reasoning models reject.

### `base_url`

Expand Down Expand Up @@ -785,12 +785,16 @@ _[number]_ - Maximum number of tokens in the response (e.g., 8192)

### `temperature`

_[number]_ - Sampling temperature to use (e.g., 0.0)
_[number]_ - Sampling temperature to use (e.g., 0.0). Defaults to the model's default. Leave unset for models that don't support it, such as Claude Opus 4.7 and later.

### `base_url`

_[string]_ - The base URL for the Claude API

### `prompt_caching`

_[boolean]_ - Cache the tools, system prompt, and conversation between requests (default false). Cache reads cost a fraction of the input price, which typically lowers cost for AI chat, but cache writes cost more than the input price.

```yaml
# Example: Claude connector configuration
type: connector
Expand Down Expand Up @@ -827,7 +831,7 @@ _[number]_ - Maximum number of tokens in the response (e.g., 8192)

### `temperature`

_[number]_ - Sampling temperature to use (0.0-2.0)
_[number]_ - Sampling temperature to use (0.0-2.0). Defaults to the model's default, which Google recommends keeping for Gemini 3 models.

### `top_p`

Expand Down
26 changes: 26 additions & 0 deletions proto/gen/rill/admin/v1/admin.swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5287,6 +5287,11 @@ definitions:
outputJsonSchema:
type: string
title: Optional output JSON schema
includeReasoning:
type: boolean
description: |-
Whether the response may include reasoning blocks.
Older clients don't support them, so they are removed from the response unless this is set.
v1CompleteResponse:
type: object
properties:
Expand All @@ -5305,6 +5310,10 @@ definitions:
type: integer
format: int64
description: Number of cache-read (discounted) input tokens; a subset of input_tokens.
cacheWriteInputTokens:
type: integer
format: int64
description: Number of input tokens written to the prompt cache, which some providers (e.g. Claude) bill at a premium.
provider:
type: string
description: The LLM provider that served the completion (e.g. "claude", "openai", "gemini").
Expand Down Expand Up @@ -5343,6 +5352,8 @@ definitions:
$ref: '#/definitions/v1ToolCall'
toolResult:
$ref: '#/definitions/v1ToolResult'
reasoning:
$ref: '#/definitions/v1Reasoning'
title: Content block within a message
v1CreateAlertResponse:
type: object
Expand Down Expand Up @@ -6792,6 +6803,21 @@ definitions:
type: string
tokensPerSeat:
type: string
v1Reasoning:
type: object
properties:
text:
type: string
description: Readable reasoning. Often empty, since providers may omit it or return a summary.
signature:
type: string
description: Opaque provider signature that lets the provider verify the reasoning.
redactedData:
type: string
description: Opaque encrypted reasoning, set instead of text and signature when the provider redacts the reasoning.
description: |-
The model's reasoning before its text or tool calls.
Some providers (e.g. Claude) require it to be passed back unmodified within a tool call loop.
v1RecordEventsRequest:
type: object
properties:
Expand Down
Loading
Loading