Add vendor assessment agent
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
@@ -152,6 +152,28 @@ func buildParams(req *llm.ChatCompletionRequest) (anthropic.MessageNewParams, er
|
||||
if req.ToolChoice != nil {
|
||||
params.ToolChoice = buildToolChoice(req.ToolChoice)
|
||||
}
|
||||
if req.Thinking != nil && req.Thinking.Enabled {
|
||||
params.Thinking = anthropic.ThinkingConfigParamOfEnabled(int64(req.Thinking.BudgetTokens))
|
||||
}
|
||||
if req.ResponseFormat != nil {
|
||||
switch req.ResponseFormat.Type {
|
||||
case llm.ResponseFormatJSONSchema:
|
||||
if req.ResponseFormat.JSONSchema == nil {
|
||||
return anthropic.MessageNewParams{}, fmt.Errorf("cannot apply JSON schema output format: schema is nil")
|
||||
}
|
||||
var schema map[string]any
|
||||
if err := json.Unmarshal(req.ResponseFormat.JSONSchema.Schema, &schema); err != nil {
|
||||
return anthropic.MessageNewParams{}, fmt.Errorf("cannot unmarshal JSON schema for output format: %w", err)
|
||||
}
|
||||
params.OutputConfig = anthropic.OutputConfigParam{
|
||||
Format: anthropic.JSONOutputFormatParam{Schema: schema},
|
||||
}
|
||||
case llm.ResponseFormatJSONObject:
|
||||
return anthropic.MessageNewParams{}, fmt.Errorf("anthropic does not support json_object response format without a schema; use json_schema instead")
|
||||
case llm.ResponseFormatText:
|
||||
// default behaviour, nothing to set
|
||||
}
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
@@ -194,12 +216,21 @@ func buildMessages(messages []llm.Message) []anthropic.MessageParam {
|
||||
out = append(out, anthropic.NewUserMessage(blocks...))
|
||||
case llm.RoleAssistant:
|
||||
var blocks []anthropic.ContentBlockParamUnion
|
||||
if text := msg.Text(); text != "" {
|
||||
blocks = append(blocks, anthropic.NewTextBlock(text))
|
||||
for _, p := range msg.Parts {
|
||||
switch part := p.(type) {
|
||||
case llm.ThinkingPart:
|
||||
blocks = append(blocks, anthropic.NewThinkingBlock(part.Signature, part.Text))
|
||||
case llm.TextPart:
|
||||
if part.Text != "" {
|
||||
blocks = append(blocks, anthropic.NewTextBlock(part.Text))
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, tc := range msg.ToolCalls {
|
||||
var input any
|
||||
_ = json.Unmarshal([]byte(tc.Function.Arguments), &input)
|
||||
if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil || input == nil {
|
||||
input = map[string]any{}
|
||||
}
|
||||
blocks = append(blocks, anthropic.NewToolUseBlock(tc.ID, input, tc.Function.Name))
|
||||
}
|
||||
out = append(out, anthropic.NewAssistantMessage(blocks...))
|
||||
@@ -295,6 +326,12 @@ func mapResponse(msg *anthropic.Message) *llm.ChatCompletionResponse {
|
||||
|
||||
for _, block := range msg.Content {
|
||||
switch block.Type {
|
||||
case "thinking":
|
||||
tb := block.AsThinking()
|
||||
resp.Message.Parts = append(resp.Message.Parts, llm.ThinkingPart{
|
||||
Text: tb.Thinking,
|
||||
Signature: tb.Signature,
|
||||
})
|
||||
case "text":
|
||||
resp.Message.Parts = append(resp.Message.Parts, llm.TextPart{Text: block.Text})
|
||||
case "tool_use":
|
||||
@@ -326,6 +363,15 @@ func mapStopReason(reason anthropic.StopReason) llm.FinishReason {
|
||||
}
|
||||
|
||||
func mapError(err error) error {
|
||||
// The Anthropic SDK refuses non-streaming requests client-side when
|
||||
// the expected response time exceeds 10 minutes (large max_tokens or
|
||||
// model-specific non-streaming token limits). It returns a plain
|
||||
// fmt.Errorf, not an *anthropic.Error, so we must match on the
|
||||
// message before attempting the type assertion.
|
||||
if err != nil && strings.Contains(err.Error(), "streaming is required") {
|
||||
return &llm.ErrStreamingRequired{Err: err}
|
||||
}
|
||||
|
||||
var apiErr *anthropic.Error
|
||||
if !errors.As(err, &apiErr) {
|
||||
return err
|
||||
@@ -361,7 +407,9 @@ type anthropicStream struct {
|
||||
stream *ssestream.Stream[anthropic.MessageStreamEventUnion]
|
||||
current llm.ChatCompletionStreamEvent
|
||||
// Track tool call indices for mapping content_block_start events.
|
||||
toolCallIndex int
|
||||
toolCallIndex int
|
||||
inToolUse bool
|
||||
thinkingSignature string
|
||||
}
|
||||
|
||||
func (s *anthropicStream) Next() bool {
|
||||
@@ -396,7 +444,9 @@ func (s *anthropicStream) mapStreamEvent(event *anthropic.MessageStreamEventUnio
|
||||
switch event.Type {
|
||||
case "content_block_start":
|
||||
cb := event.ContentBlock
|
||||
if cb.Type == "tool_use" {
|
||||
switch cb.Type {
|
||||
case "tool_use":
|
||||
s.inToolUse = true
|
||||
tu := cb.AsToolUse()
|
||||
return llm.ChatCompletionStreamEvent{
|
||||
Delta: llm.MessageDelta{
|
||||
@@ -407,6 +457,8 @@ func (s *anthropicStream) mapStreamEvent(event *anthropic.MessageStreamEventUnio
|
||||
}},
|
||||
},
|
||||
}, true
|
||||
case "thinking":
|
||||
return llm.ChatCompletionStreamEvent{}, false
|
||||
}
|
||||
return llm.ChatCompletionStreamEvent{}, false
|
||||
|
||||
@@ -417,6 +469,15 @@ func (s *anthropicStream) mapStreamEvent(event *anthropic.MessageStreamEventUnio
|
||||
return llm.ChatCompletionStreamEvent{
|
||||
Delta: llm.MessageDelta{Content: delta.Text},
|
||||
}, true
|
||||
case "thinking_delta":
|
||||
return llm.ChatCompletionStreamEvent{
|
||||
Delta: llm.MessageDelta{Thinking: delta.Thinking},
|
||||
}, true
|
||||
case "signature_delta":
|
||||
s.thinkingSignature = delta.Signature
|
||||
return llm.ChatCompletionStreamEvent{
|
||||
Delta: llm.MessageDelta{ThinkingSignature: delta.Signature},
|
||||
}, true
|
||||
case "input_json_delta":
|
||||
return llm.ChatCompletionStreamEvent{
|
||||
Delta: llm.MessageDelta{
|
||||
@@ -430,8 +491,9 @@ func (s *anthropicStream) mapStreamEvent(event *anthropic.MessageStreamEventUnio
|
||||
return llm.ChatCompletionStreamEvent{}, false
|
||||
|
||||
case "content_block_stop":
|
||||
if event.ContentBlock.Type == "tool_use" {
|
||||
if s.inToolUse {
|
||||
s.toolCallIndex++
|
||||
s.inToolUse = false
|
||||
}
|
||||
return llm.ChatCompletionStreamEvent{}, false
|
||||
|
||||
|
||||
@@ -33,6 +33,12 @@ type (
|
||||
ToolChoice *ToolChoice
|
||||
ParallelToolCalls *bool
|
||||
ResponseFormat *ResponseFormat
|
||||
Thinking *ThinkingConfig
|
||||
}
|
||||
|
||||
ThinkingConfig struct {
|
||||
Enabled bool
|
||||
BudgetTokens int
|
||||
}
|
||||
|
||||
ToolChoiceType string
|
||||
@@ -97,8 +103,10 @@ type (
|
||||
}
|
||||
|
||||
MessageDelta struct {
|
||||
Content string
|
||||
ToolCalls []ToolCallDelta
|
||||
Content string
|
||||
Thinking string
|
||||
ThinkingSignature string
|
||||
ToolCalls []ToolCallDelta
|
||||
}
|
||||
|
||||
ToolCallDelta struct {
|
||||
@@ -144,13 +152,15 @@ func (u Usage) Add(other Usage) Usage {
|
||||
// After the stream is exhausted (Next returns false), call Response
|
||||
// to get the fully assembled ChatCompletionResponse.
|
||||
type StreamAccumulator struct {
|
||||
stream ChatCompletionStream
|
||||
current ChatCompletionStreamEvent
|
||||
content strings.Builder
|
||||
toolCalls map[int]*ToolCall
|
||||
usage Usage
|
||||
finishReason FinishReason
|
||||
model string
|
||||
stream ChatCompletionStream
|
||||
current ChatCompletionStreamEvent
|
||||
content strings.Builder
|
||||
thinking strings.Builder
|
||||
thinkingSignature string
|
||||
toolCalls map[int]*ToolCall
|
||||
usage Usage
|
||||
finishReason FinishReason
|
||||
model string
|
||||
}
|
||||
|
||||
func NewStreamAccumulator(stream ChatCompletionStream) *StreamAccumulator {
|
||||
@@ -194,11 +204,20 @@ func (a *StreamAccumulator) Response() *ChatCompletionResponse {
|
||||
}
|
||||
}
|
||||
|
||||
var parts []Part
|
||||
if thinking := a.thinking.String(); thinking != "" {
|
||||
parts = append(parts, ThinkingPart{
|
||||
Text: thinking,
|
||||
Signature: a.thinkingSignature,
|
||||
})
|
||||
}
|
||||
parts = append(parts, TextPart{Text: a.content.String()})
|
||||
|
||||
return &ChatCompletionResponse{
|
||||
Model: a.model,
|
||||
Message: Message{
|
||||
Role: RoleAssistant,
|
||||
Parts: []Part{TextPart{Text: a.content.String()}},
|
||||
Parts: parts,
|
||||
ToolCalls: toolCalls,
|
||||
},
|
||||
Usage: a.usage,
|
||||
@@ -212,6 +231,10 @@ func (a *StreamAccumulator) accumulate(event ChatCompletionStreamEvent) {
|
||||
}
|
||||
|
||||
a.content.WriteString(event.Delta.Content)
|
||||
a.thinking.WriteString(event.Delta.Thinking)
|
||||
if event.Delta.ThinkingSignature != "" {
|
||||
a.thinkingSignature = event.Delta.ThinkingSignature
|
||||
}
|
||||
|
||||
for _, tcd := range event.Delta.ToolCalls {
|
||||
tc, ok := a.toolCalls[tcd.Index]
|
||||
|
||||
@@ -37,6 +37,13 @@ type (
|
||||
ErrAuthentication struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
// ErrStreamingRequired is returned by a provider when a non-streaming
|
||||
// request must be retried with the streaming endpoint (e.g. Anthropic
|
||||
// requires streaming for responses that may take longer than 10 minutes).
|
||||
ErrStreamingRequired struct {
|
||||
Err error
|
||||
}
|
||||
)
|
||||
|
||||
func (e *ErrRateLimit) Error() string {
|
||||
@@ -68,3 +75,9 @@ func (e *ErrAuthentication) Error() string {
|
||||
}
|
||||
|
||||
func (e *ErrAuthentication) Unwrap() error { return e.Err }
|
||||
|
||||
func (e *ErrStreamingRequired) Error() string {
|
||||
return fmt.Sprintf("streaming is required: %v", e.Err)
|
||||
}
|
||||
|
||||
func (e *ErrStreamingRequired) Unwrap() error { return e.Err }
|
||||
|
||||
@@ -52,3 +52,13 @@ func (m Message) Text() string {
|
||||
}
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func (m Message) Thinking() string {
|
||||
var s strings.Builder
|
||||
for _, p := range m.Parts {
|
||||
if tp, ok := p.(ThinkingPart); ok {
|
||||
s.WriteString(tp.Text)
|
||||
}
|
||||
}
|
||||
return s.String()
|
||||
}
|
||||
|
||||
@@ -166,6 +166,16 @@ func buildParams(req *llm.ChatCompletionRequest) openai.ChatCompletionNewParams
|
||||
if req.ResponseFormat != nil {
|
||||
params.ResponseFormat = buildResponseFormat(req.ResponseFormat)
|
||||
}
|
||||
if req.Thinking != nil && req.Thinking.Enabled && isReasoningModel(req.Model) {
|
||||
switch {
|
||||
case req.Thinking.BudgetTokens <= 1024:
|
||||
params.ReasoningEffort = shared.ReasoningEffortLow
|
||||
case req.Thinking.BudgetTokens <= 8192:
|
||||
params.ReasoningEffort = shared.ReasoningEffortMedium
|
||||
default:
|
||||
params.ReasoningEffort = shared.ReasoningEffortHigh
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
@@ -456,6 +466,17 @@ func mapChunkToEvent(chunk *openai.ChatCompletionChunk) llm.ChatCompletionStream
|
||||
return event
|
||||
}
|
||||
|
||||
// isReasoningModel returns true for OpenAI models that support
|
||||
// reasoning_effort (o1, o3-mini, o3, and their dated variants).
|
||||
func isReasoningModel(model string) bool {
|
||||
for _, prefix := range []string{"o1", "o3"} {
|
||||
if model == prefix || strings.HasPrefix(model, prefix+"-") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func buildFilePart(p llm.FilePart) openai.ChatCompletionContentPartUnionParam {
|
||||
switch {
|
||||
case strings.HasPrefix(p.MimeType, "image/"):
|
||||
|
||||
@@ -32,8 +32,14 @@ type (
|
||||
MimeType string // e.g. "application/pdf", "text/csv", "image/png"
|
||||
Filename string
|
||||
}
|
||||
|
||||
ThinkingPart struct {
|
||||
Text string
|
||||
Signature string // Anthropic thinking signature for multi-turn continuity
|
||||
}
|
||||
)
|
||||
|
||||
func (TextPart) part() {}
|
||||
func (ImagePart) part() {}
|
||||
func (FilePart) part() {}
|
||||
func (TextPart) part() {}
|
||||
func (ImagePart) part() {}
|
||||
func (FilePart) part() {}
|
||||
func (ThinkingPart) part() {}
|
||||
|
||||
Reference in New Issue
Block a user