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
3 changes: 2 additions & 1 deletion admin/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,8 @@ func (s *Server) jwtAttributesForService(ctx context.Context, serviceID string,

func timeoutSelector(fullMethodName string) time.Duration {
if strings.HasPrefix(fullMethodName, "/rill.admin.v1.AIService") {
return time.Minute * 2
// NOTE: The runtime usually sets a lower timeout through its AILLMTimeoutSeconds config, so this is more of a hard upper bound.
return time.Minute * 10
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

llm_timeout_seconds is configurable now through instance config so does this mean setting anything beyond 10 mins is just not useful?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the 10 mins here will be a hard-cap for when using Rill's AI service (won't apply if you bring your own token). Since generating tokens for 10 mins can already run up quite a bill, I think it's safest not to allow calls longer than this. What do you think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good, was just thinking to document somewhere that setting llm_timeout_seconds more than 10 minutes won't help so that user is aware.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, just added docs about it

}
if fullMethodName == "/rill.admin.v1.AdminService/DeleteProject" {
return time.Minute * 4
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/reference/project-files/rill-yaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ _[object]_ - A map of key-value pairs for setting variables on your project. It

- **`rill.ai.completion_timeout_seconds`** - _[integer]_ - Maximum duration of a full AI completion request (which may include multiple LLM calls and tool uses), in seconds. Default: 300.

- **`rill.ai.llm_timeout_seconds`** - _[integer]_ - Maximum duration of a single LLM completion request, in seconds. Default: 180.
- **`rill.ai.llm_timeout_seconds`** - _[integer]_ - Maximum duration of a single LLM completion request, in seconds. Default: 180. Note: when using Rill's hosted AI service (i.e. not a self-configured LLM), the admin server enforces a hard upper bound of 10 minutes, so values above that have no effect.

- **`rill.ai.default_query_limit`** - _[integer]_ - Default row limit applied to AI tool queries when no limit is specified. Default: 25.

Expand Down
10 changes: 9 additions & 1 deletion runtime/ai/ai.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb"
)

Expand Down Expand Up @@ -1278,9 +1280,15 @@ func (s *Session) Complete(ctx context.Context, name string, out any, opts *Comp

// Handle LLM completion error
if err != nil {
if errors.Is(err, llmCtx.Err()) && errors.Is(err, context.DeadlineExceeded) {
if errors.Is(err, llmCtx.Err()) && errors.Is(err, context.DeadlineExceeded) { // Timeout from local ctx.
return nil, fmt.Errorf("LLM request timed out after %s: %w", llmRequestTimeout, err)
}
if status.Code(err) == codes.DeadlineExceeded { // Timeout from admin service.
return nil, fmt.Errorf("LLM request timed out: %w", err)
}
if errors.Is(err, ctx.Err()) {
return nil, ctx.Err()
}
return nil, fmt.Errorf("completion failed: %w (stack: %s)", err, string(debug.Stack()))
}

Expand Down
21 changes: 18 additions & 3 deletions runtime/ai/router_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ai

import (
"context"
"errors"
"fmt"
"regexp"
"slices"
Expand Down Expand Up @@ -165,7 +166,7 @@ func (t *RouterAgent) Handler(ctx context.Context, args *RouterAgentArgs) (*Rout
Args: analystAgentArgs,
})
if err != nil {
return nil, err
return nil, mapAgentErr(err)
}
return &RouterAgentResult{Response: res.Response, Agent: args.Agent}, nil

Expand All @@ -184,7 +185,7 @@ func (t *RouterAgent) Handler(ctx context.Context, args *RouterAgentArgs) (*Rout
Args: developerAgentArgs,
})
if err != nil {
return nil, err
return nil, mapAgentErr(err)
}
return &RouterAgentResult{Response: res.Response, Agent: args.Agent}, nil

Expand All @@ -197,7 +198,7 @@ func (t *RouterAgent) Handler(ctx context.Context, args *RouterAgentArgs) (*Rout
Args: args.FeedbackAgentArgs,
})
if err != nil {
return nil, err
return nil, mapAgentErr(err)
}
return &RouterAgentResult{Response: res.Response, Agent: FeedbackAgentName}, nil
}
Expand Down Expand Up @@ -245,6 +246,20 @@ func promptToTitle(message string) string {
return title
}

// mapAgentErr maps common agent errors to more user-friendly messages.
//
// NOTE: For context errors, it does not include the underlying error to keep messages clean.
// The actual error is still available in the message containing the sub-agent's result.
func mapAgentErr(err error) error {
if errors.Is(err, context.Canceled) {
return fmt.Errorf("agent canceled")
}
if errors.Is(err, context.DeadlineExceeded) {
return fmt.Errorf("agent timed out")
}
return fmt.Errorf("agent error: %w", err)
}

func must[T any](t T, ok bool) T {
if !ok {
panic("expected value to be present")
Expand Down
1 change: 1 addition & 0 deletions runtime/drivers/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ type InstanceConfig struct {
// AICompletionTimeoutSeconds is the maximum duration of a full AI completion request, which may include multiple LLM requests and tool calls.
AICompletionTimeoutSeconds uint32 `mapstructure:"rill.ai.completion_timeout_seconds"`
// AILLMTimeoutSeconds is the maximum duration of a single LLM completion request.
// Note: when using Rill's hosted AI service (i.e. not a self-configured LLM), the admin server enforces a hard upper bound of 10 minutes, so values above that have no effect.
AILLMTimeoutSeconds uint32 `mapstructure:"rill.ai.llm_timeout_seconds"`
// AIDefaultQueryLimit is the default row limit applied to AI tool queries when no limit is specified.
AIDefaultQueryLimit int64 `mapstructure:"rill.ai.default_query_limit"`
Expand Down
2 changes: 1 addition & 1 deletion runtime/parser/schema/rillyaml.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ allOf:
description: "Maximum duration of a full AI completion request (which may include multiple LLM calls and tool uses), in seconds. Default: 300."
rill.ai.llm_timeout_seconds:
type: integer
description: "Maximum duration of a single LLM completion request, in seconds. Default: 180."
description: "Maximum duration of a single LLM completion request, in seconds. Default: 180. Note: when using Rill's hosted AI service (i.e. not a self-configured LLM), the admin server enforces a hard upper bound of 10 minutes, so values above that have no effect."
rill.ai.default_query_limit:
type: integer
description: "Default row limit applied to AI tool queries when no limit is specified. Default: 25."
Expand Down
Loading