feat: native SAP AI Core support
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 14s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 35s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m30s
CI / review (gpt-5, security, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 2m0s
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 14s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 35s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m30s
CI / review (gpt-5, security, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 2m0s
Add native SAP AI Core provider that handles OAuth token management and deployment discovery automatically. This eliminates the need for the external LLM proxy when running in SAP environments. Changes: - Add AICoreClient with OAuth token caching and deployment URL discovery - Support both Anthropic and OpenAI models via AI Core deployments - Update CI to use native AI Core provider - Update action inputs to accept AI Core credentials - Update README with AI Core configuration examples Model names must match AI Core deployment names (e.g. anthropic--claude-4.6-sonnet, gpt-5).
This commit is contained in:
+38
-7
@@ -1,6 +1,6 @@
|
||||
// Package llm provides clients for LLM chat completion APIs.
|
||||
//
|
||||
// Supports OpenAI-compatible (default) and Anthropic Messages API providers.
|
||||
// Supports OpenAI-compatible (default), Anthropic Messages API, and SAP AI Core providers.
|
||||
package llm
|
||||
|
||||
import (
|
||||
@@ -22,6 +22,8 @@ const (
|
||||
ProviderOpenAI Provider = "openai"
|
||||
// ProviderAnthropic uses the Anthropic Messages API endpoint.
|
||||
ProviderAnthropic Provider = "anthropic"
|
||||
// ProviderAICore uses SAP AI Core with OAuth authentication.
|
||||
ProviderAICore Provider = "aicore"
|
||||
)
|
||||
|
||||
// Client calls an LLM chat completion API.
|
||||
@@ -35,6 +37,7 @@ type Client struct {
|
||||
temperature float64
|
||||
provider Provider
|
||||
http *http.Client
|
||||
aicore *AICoreClient // Only set when provider is aicore
|
||||
}
|
||||
|
||||
// NewClient creates a new LLM client. Default provider is OpenAI-compatible.
|
||||
@@ -49,8 +52,12 @@ func NewClient(baseURL, apiKey, model string) *Client {
|
||||
}
|
||||
|
||||
// WithTimeout sets the HTTP request timeout for LLM calls (default 5 minutes).
|
||||
// When using AI Core, this also sets the timeout on the AI Core client.
|
||||
func (c *Client) WithTimeout(d time.Duration) *Client {
|
||||
c.http.Timeout = d
|
||||
if c.aicore != nil {
|
||||
c.aicore.WithTimeout(d)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -60,12 +67,21 @@ func (c *Client) WithTemperature(t float64) *Client {
|
||||
return c
|
||||
}
|
||||
|
||||
// WithProvider sets the API provider format (openai or anthropic).
|
||||
// WithProvider sets the API provider format (openai, anthropic, or aicore).
|
||||
func (c *Client) WithProvider(p Provider) *Client {
|
||||
c.provider = p
|
||||
return c
|
||||
}
|
||||
|
||||
// WithAICore configures the client to use SAP AI Core for authentication.
|
||||
// This sets the provider to aicore automatically.
|
||||
// The AI Core client inherits the current HTTP timeout from this client.
|
||||
func (c *Client) WithAICore(cfg AICoreConfig) *Client {
|
||||
c.provider = ProviderAICore
|
||||
c.aicore = NewAICoreClient(cfg).WithTimeout(c.http.Timeout)
|
||||
return c
|
||||
}
|
||||
|
||||
// Message represents a chat message.
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
@@ -82,6 +98,8 @@ func (c *Client) Complete(ctx context.Context, messages []Message) (string, erro
|
||||
switch c.provider {
|
||||
case ProviderAnthropic:
|
||||
result, err = c.completeAnthropic(ctx, messages)
|
||||
case ProviderAICore:
|
||||
result, err = c.completeAICore(ctx, messages)
|
||||
default:
|
||||
result, err = c.completeOpenAI(ctx, messages)
|
||||
}
|
||||
@@ -106,6 +124,18 @@ func (c *Client) Complete(ctx context.Context, messages []Message) (string, erro
|
||||
return "", err
|
||||
}
|
||||
|
||||
// completeAICore routes to AI Core using the appropriate endpoint based on model type.
|
||||
func (c *Client) completeAICore(ctx context.Context, messages []Message) (string, error) {
|
||||
if c.aicore == nil {
|
||||
return "", fmt.Errorf("AI Core client not configured")
|
||||
}
|
||||
|
||||
if IsAnthropicModel(c.model) {
|
||||
return c.aicore.CompleteAnthropic(ctx, c.model, messages, 8192, c.temperature)
|
||||
}
|
||||
return c.aicore.CompleteOpenAI(ctx, c.model, messages, c.temperature)
|
||||
}
|
||||
|
||||
// isRetryableError returns true for transient errors worth retrying.
|
||||
func isRetryableError(err error) bool {
|
||||
if err == nil {
|
||||
@@ -176,11 +206,12 @@ func (c *Client) completeOpenAI(ctx context.Context, messages []Message) (string
|
||||
// --- Anthropic Messages API implementation ---
|
||||
|
||||
type anthropicRequest struct {
|
||||
Model string `json:"model"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
System string `json:"system,omitempty"`
|
||||
Messages []anthropicMsg `json:"messages"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
AnthropicVersion string `json:"anthropic_version,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
System string `json:"system,omitempty"`
|
||||
Messages []anthropicMsg `json:"messages"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
}
|
||||
|
||||
type anthropicMsg struct {
|
||||
|
||||
Reference in New Issue
Block a user