2026-02-15 13:04:12 +00:00
|
|
|
package anthropicprovider
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
2026-02-17 16:13:10 +00:00
|
|
|
"log"
|
|
|
|
|
"strings"
|
2026-02-15 13:04:12 +00:00
|
|
|
|
|
|
|
|
"github.com/anthropics/anthropic-sdk-go"
|
|
|
|
|
"github.com/anthropics/anthropic-sdk-go/option"
|
2026-02-18 19:48:23 +00:00
|
|
|
|
2026-04-19 04:20:00 +00:00
|
|
|
"github.com/sipeed/picoclaw/pkg/providers/common"
|
2026-02-17 16:13:10 +00:00
|
|
|
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
2026-02-15 13:04:12 +00:00
|
|
|
)
|
|
|
|
|
|
2026-02-18 19:48:23 +00:00
|
|
|
type (
|
|
|
|
|
ToolCall = protocoltypes.ToolCall
|
|
|
|
|
FunctionCall = protocoltypes.FunctionCall
|
|
|
|
|
LLMResponse = protocoltypes.LLMResponse
|
|
|
|
|
UsageInfo = protocoltypes.UsageInfo
|
|
|
|
|
Message = protocoltypes.Message
|
|
|
|
|
ToolDefinition = protocoltypes.ToolDefinition
|
|
|
|
|
ToolFunctionDefinition = protocoltypes.ToolFunctionDefinition
|
|
|
|
|
)
|
2026-02-15 13:04:12 +00:00
|
|
|
|
2026-03-06 11:58:23 +00:00
|
|
|
const (
|
|
|
|
|
defaultBaseURL = "https://api.anthropic.com"
|
|
|
|
|
anthropicBetaHeader = "oauth-2025-04-20"
|
|
|
|
|
)
|
2026-02-15 13:04:12 +00:00
|
|
|
|
|
|
|
|
type Provider struct {
|
|
|
|
|
client *anthropic.Client
|
|
|
|
|
tokenSource func() (string, error)
|
2026-02-17 16:13:10 +00:00
|
|
|
baseURL string
|
2026-02-15 13:04:12 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-05 01:51:18 +00:00
|
|
|
// SupportsThinking implements providers.ThinkingCapable.
|
|
|
|
|
func (p *Provider) SupportsThinking() bool { return true }
|
|
|
|
|
|
2026-02-15 13:04:12 +00:00
|
|
|
func NewProvider(token string) *Provider {
|
2026-02-17 16:13:10 +00:00
|
|
|
return NewProviderWithBaseURL(token, "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewProviderWithBaseURL(token, apiBase string) *Provider {
|
2026-04-19 06:48:28 +00:00
|
|
|
baseURL := common.NormalizeBaseURL(apiBase, defaultBaseURL, false)
|
2026-02-15 13:04:12 +00:00
|
|
|
client := anthropic.NewClient(
|
|
|
|
|
option.WithAuthToken(token),
|
2026-02-17 16:13:10 +00:00
|
|
|
option.WithBaseURL(baseURL),
|
2026-02-15 13:04:12 +00:00
|
|
|
)
|
2026-02-17 16:13:10 +00:00
|
|
|
return &Provider{
|
|
|
|
|
client: &client,
|
|
|
|
|
baseURL: baseURL,
|
|
|
|
|
}
|
2026-02-15 13:04:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewProviderWithClient(client *anthropic.Client) *Provider {
|
2026-02-17 16:13:10 +00:00
|
|
|
return &Provider{
|
|
|
|
|
client: client,
|
|
|
|
|
baseURL: defaultBaseURL,
|
|
|
|
|
}
|
2026-02-15 13:04:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewProviderWithTokenSource(token string, tokenSource func() (string, error)) *Provider {
|
2026-02-17 16:13:10 +00:00
|
|
|
return NewProviderWithTokenSourceAndBaseURL(token, tokenSource, "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewProviderWithTokenSourceAndBaseURL(token string, tokenSource func() (string, error), apiBase string) *Provider {
|
|
|
|
|
p := NewProviderWithBaseURL(token, apiBase)
|
2026-02-15 13:04:12 +00:00
|
|
|
p.tokenSource = tokenSource
|
|
|
|
|
return p
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 19:48:23 +00:00
|
|
|
func (p *Provider) Chat(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
messages []Message,
|
|
|
|
|
tools []ToolDefinition,
|
|
|
|
|
model string,
|
|
|
|
|
options map[string]any,
|
|
|
|
|
) (*LLMResponse, error) {
|
2026-02-15 13:04:12 +00:00
|
|
|
var opts []option.RequestOption
|
|
|
|
|
if p.tokenSource != nil {
|
|
|
|
|
tok, err := p.tokenSource()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("refreshing token: %w", err)
|
|
|
|
|
}
|
2026-03-06 11:58:23 +00:00
|
|
|
opts = append(opts,
|
|
|
|
|
option.WithAuthToken(tok),
|
|
|
|
|
option.WithHeader("anthropic-beta", anthropicBetaHeader),
|
|
|
|
|
)
|
2026-02-15 13:04:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
params, err := buildParams(messages, tools, model, options)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 11:58:23 +00:00
|
|
|
// OAuth/setup-tokens require streaming; API keys use non-streaming.
|
|
|
|
|
if p.tokenSource != nil {
|
|
|
|
|
return p.chatStreaming(ctx, params, opts)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 13:04:12 +00:00
|
|
|
resp, err := p.client.Messages.New(ctx, params, opts...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("claude API call: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return parseResponse(resp), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 11:58:23 +00:00
|
|
|
func (p *Provider) chatStreaming(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
params anthropic.MessageNewParams,
|
|
|
|
|
opts []option.RequestOption,
|
|
|
|
|
) (*LLMResponse, error) {
|
|
|
|
|
stream := p.client.Messages.NewStreaming(ctx, params, opts...)
|
|
|
|
|
defer stream.Close()
|
|
|
|
|
|
|
|
|
|
var msg anthropic.Message
|
|
|
|
|
for stream.Next() {
|
|
|
|
|
event := stream.Current()
|
|
|
|
|
if err := msg.Accumulate(event); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("claude streaming accumulate: %w", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := stream.Err(); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("claude API call: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return parseResponse(&msg), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 13:04:12 +00:00
|
|
|
func (p *Provider) GetDefaultModel() string {
|
2026-02-20 04:15:04 +00:00
|
|
|
return "claude-sonnet-4.6"
|
2026-02-15 13:04:12 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 16:13:10 +00:00
|
|
|
func (p *Provider) BaseURL() string {
|
|
|
|
|
return p.baseURL
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 19:48:23 +00:00
|
|
|
func buildParams(
|
|
|
|
|
messages []Message,
|
|
|
|
|
tools []ToolDefinition,
|
|
|
|
|
model string,
|
|
|
|
|
options map[string]any,
|
|
|
|
|
) (anthropic.MessageNewParams, error) {
|
2026-02-15 13:04:12 +00:00
|
|
|
var system []anthropic.TextBlockParam
|
|
|
|
|
var anthropicMessages []anthropic.MessageParam
|
|
|
|
|
|
|
|
|
|
for _, msg := range messages {
|
|
|
|
|
switch msg.Role {
|
|
|
|
|
case "system":
|
fix: cache system prompt with mtime-based auto-invalidation (#607)
Avoid rebuilding the entire system prompt on every BuildMessages() call
by caching the static portion (identity, bootstrap, skills summary,
memory) and only recomputing it when workspace source files change.
Key changes:
- ContextBuilder caches the static prompt behind an RWMutex with
double-checked locking. Source file changes are detected via cheap
os.Stat mtime checks so no explicit invalidation is needed.
- Track file existence at cache time (existedAtCache map) so that
newly created or deleted bootstrap/memory files also trigger a
rebuild — the old modifiedSince() silently returned false on
os.IsNotExist.
- Walk the skills directory recursively with filepath.WalkDir to
catch content-only edits at any nesting depth; directory mtime
alone misses in-place file modifications on most filesystems.
- ToolRegistry.sortedToolNames() sorts tool names before iteration,
ensuring deterministic tool definition order across calls — a
prerequisite for LLM-side prefix/KV cache reuse.
- Merge all context (static + dynamic + summary) into a single
system message for provider compatibility: the Anthropic adapter
extracts messages[0] as the top-level system parameter, and Codex
reads only the first system message as instructions.
- Fix a data race in BuildMessages() where cachedSystemPrompt was
read without holding the lock in a debug log statement.
- Add tests: single system message invariant, mtime auto-invalidation,
new-file creation detection, skill file content change, explicit
InvalidateCache, cache stability, concurrent access (20 goroutines
x 50 iterations, passes go test -race), and a benchmark.
2026-02-25 02:34:54 +00:00
|
|
|
// Prefer structured SystemParts for per-block cache_control.
|
|
|
|
|
// This enables LLM-side KV cache reuse: the static block's prefix
|
|
|
|
|
// hash stays stable across requests while dynamic parts change freely.
|
|
|
|
|
if len(msg.SystemParts) > 0 {
|
|
|
|
|
for _, part := range msg.SystemParts {
|
|
|
|
|
block := anthropic.TextBlockParam{Text: part.Text}
|
|
|
|
|
if part.CacheControl != nil && part.CacheControl.Type == "ephemeral" {
|
|
|
|
|
block.CacheControl = anthropic.NewCacheControlEphemeralParam()
|
|
|
|
|
}
|
|
|
|
|
system = append(system, block)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
system = append(system, anthropic.TextBlockParam{Text: msg.Content})
|
|
|
|
|
}
|
2026-02-15 13:04:12 +00:00
|
|
|
case "user":
|
|
|
|
|
if msg.ToolCallID != "" {
|
|
|
|
|
anthropicMessages = append(anthropicMessages,
|
|
|
|
|
anthropic.NewUserMessage(anthropic.NewToolResultBlock(msg.ToolCallID, msg.Content, false)),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
anthropicMessages = append(anthropicMessages,
|
|
|
|
|
anthropic.NewUserMessage(anthropic.NewTextBlock(msg.Content)),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
case "assistant":
|
|
|
|
|
if len(msg.ToolCalls) > 0 {
|
|
|
|
|
var blocks []anthropic.ContentBlockParamUnion
|
|
|
|
|
if msg.Content != "" {
|
|
|
|
|
blocks = append(blocks, anthropic.NewTextBlock(msg.Content))
|
|
|
|
|
}
|
|
|
|
|
for _, tc := range msg.ToolCalls {
|
2026-03-18 13:55:01 +00:00
|
|
|
// Skip tool calls with empty names to avoid API errors
|
|
|
|
|
if tc.Name == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-03-06 11:58:23 +00:00
|
|
|
args := tc.Arguments
|
|
|
|
|
if args == nil && tc.Function != nil && tc.Function.Arguments != "" {
|
|
|
|
|
if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
|
|
|
|
|
args = map[string]any{}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if args == nil {
|
|
|
|
|
args = map[string]any{}
|
|
|
|
|
}
|
|
|
|
|
blocks = append(blocks, anthropic.NewToolUseBlock(tc.ID, args, tc.Name))
|
2026-02-15 13:04:12 +00:00
|
|
|
}
|
|
|
|
|
anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(blocks...))
|
|
|
|
|
} else {
|
|
|
|
|
anthropicMessages = append(anthropicMessages,
|
|
|
|
|
anthropic.NewAssistantMessage(anthropic.NewTextBlock(msg.Content)),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
case "tool":
|
|
|
|
|
anthropicMessages = append(anthropicMessages,
|
|
|
|
|
anthropic.NewUserMessage(anthropic.NewToolResultBlock(msg.ToolCallID, msg.Content, false)),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
maxTokens := int64(4096)
|
|
|
|
|
if mt, ok := options["max_tokens"].(int); ok {
|
|
|
|
|
maxTokens = int64(mt)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 11:58:23 +00:00
|
|
|
// Normalize model ID: Anthropic API uses hyphens (claude-sonnet-4-6),
|
|
|
|
|
// but config may use dots (claude-sonnet-4.6).
|
|
|
|
|
apiModel := strings.ReplaceAll(model, ".", "-")
|
|
|
|
|
|
2026-02-15 13:04:12 +00:00
|
|
|
params := anthropic.MessageNewParams{
|
2026-03-06 11:58:23 +00:00
|
|
|
Model: anthropic.Model(apiModel),
|
2026-02-15 13:04:12 +00:00
|
|
|
Messages: anthropicMessages,
|
|
|
|
|
MaxTokens: maxTokens,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(system) > 0 {
|
|
|
|
|
params.System = system
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if temp, ok := options["temperature"].(float64); ok {
|
|
|
|
|
params.Temperature = anthropic.Float(temp)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(tools) > 0 {
|
|
|
|
|
params.Tools = translateTools(tools)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 01:51:18 +00:00
|
|
|
// Extended Thinking / Adaptive Thinking
|
|
|
|
|
// The thinking_level value directly determines the API parameter format:
|
|
|
|
|
// "adaptive" → {thinking: {type: "adaptive"}} + output_config.effort
|
|
|
|
|
// "low/medium/high/xhigh" → {thinking: {type: "enabled", budget_tokens: N}}
|
|
|
|
|
if level, ok := options["thinking_level"].(string); ok && level != "" && level != "off" {
|
|
|
|
|
applyThinkingConfig(¶ms, level)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 13:04:12 +00:00
|
|
|
return params, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 01:51:18 +00:00
|
|
|
// applyThinkingConfig sets thinking parameters based on the level value.
|
|
|
|
|
// "adaptive" uses the adaptive thinking API (Claude 4.6+).
|
|
|
|
|
// All other levels use budget_tokens which is universally supported.
|
|
|
|
|
//
|
|
|
|
|
// Anthropic API constraint: temperature must not be set when thinking is enabled.
|
|
|
|
|
// budget_tokens must be strictly less than max_tokens.
|
|
|
|
|
func applyThinkingConfig(params *anthropic.MessageNewParams, level string) {
|
|
|
|
|
// Anthropic API rejects requests with temperature set alongside thinking.
|
|
|
|
|
// Reset to zero value (omitted from JSON serialization).
|
|
|
|
|
if params.Temperature.Valid() {
|
|
|
|
|
log.Printf("anthropic: temperature cleared because thinking is enabled (level=%s)", level)
|
|
|
|
|
}
|
|
|
|
|
params.Temperature = anthropic.MessageNewParams{}.Temperature
|
|
|
|
|
|
|
|
|
|
if level == "adaptive" {
|
|
|
|
|
adaptive := anthropic.NewThinkingConfigAdaptiveParam()
|
|
|
|
|
params.Thinking = anthropic.ThinkingConfigParamUnion{OfAdaptive: &adaptive}
|
|
|
|
|
params.OutputConfig = anthropic.OutputConfigParam{
|
|
|
|
|
Effort: anthropic.OutputConfigEffortHigh,
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
budget := int64(levelToBudget(level))
|
|
|
|
|
if budget <= 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// budget_tokens must be < max_tokens; clamp to respect user's max_tokens setting.
|
|
|
|
|
if budget >= params.MaxTokens {
|
|
|
|
|
log.Printf("anthropic: budget_tokens (%d) clamped to %d (max_tokens-1)", budget, params.MaxTokens-1)
|
|
|
|
|
budget = params.MaxTokens - 1
|
|
|
|
|
} else if budget > params.MaxTokens*80/100 {
|
|
|
|
|
log.Printf("anthropic: thinking budget (%d) exceeds 80%% of max_tokens (%d), output may be truncated",
|
|
|
|
|
budget, params.MaxTokens)
|
|
|
|
|
}
|
|
|
|
|
params.Thinking = anthropic.ThinkingConfigParamOfEnabled(budget)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// levelToBudget maps a thinking level to budget_tokens.
|
|
|
|
|
// Values are based on Anthropic's recommendations and community best practices:
|
|
|
|
|
//
|
|
|
|
|
// low = 4,096 — simple reasoning, quick debugging (Claude Code "think")
|
|
|
|
|
// medium = 16,384 — Anthropic recommended sweet spot for most tasks
|
|
|
|
|
// high = 32,000 — complex architecture, deep analysis (diminishing returns above this)
|
|
|
|
|
// xhigh = 64,000 — extreme reasoning, research problems, benchmarks
|
|
|
|
|
//
|
|
|
|
|
// Note: For Claude 4.6+, prefer adaptive thinking over manual budget_tokens.
|
|
|
|
|
func levelToBudget(level string) int {
|
|
|
|
|
switch level {
|
|
|
|
|
case "low":
|
|
|
|
|
return 4096
|
|
|
|
|
case "medium":
|
|
|
|
|
return 16384
|
|
|
|
|
case "high":
|
|
|
|
|
return 32000
|
|
|
|
|
case "xhigh":
|
|
|
|
|
return 64000
|
|
|
|
|
default:
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 13:04:12 +00:00
|
|
|
func translateTools(tools []ToolDefinition) []anthropic.ToolUnionParam {
|
|
|
|
|
result := make([]anthropic.ToolUnionParam, 0, len(tools))
|
|
|
|
|
for _, t := range tools {
|
|
|
|
|
tool := anthropic.ToolParam{
|
|
|
|
|
Name: t.Function.Name,
|
|
|
|
|
InputSchema: anthropic.ToolInputSchemaParam{
|
|
|
|
|
Properties: t.Function.Parameters["properties"],
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
if desc := t.Function.Description; desc != "" {
|
|
|
|
|
tool.Description = anthropic.String(desc)
|
|
|
|
|
}
|
2026-02-18 19:48:23 +00:00
|
|
|
if req, ok := t.Function.Parameters["required"].([]any); ok {
|
2026-02-15 13:04:12 +00:00
|
|
|
required := make([]string, 0, len(req))
|
|
|
|
|
for _, r := range req {
|
|
|
|
|
if s, ok := r.(string); ok {
|
|
|
|
|
required = append(required, s)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
tool.InputSchema.Required = required
|
|
|
|
|
}
|
|
|
|
|
result = append(result, anthropic.ToolUnionParam{OfTool: &tool})
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseResponse(resp *anthropic.Message) *LLMResponse {
|
2026-02-27 08:35:07 +00:00
|
|
|
var content strings.Builder
|
2026-03-05 01:51:18 +00:00
|
|
|
var reasoning strings.Builder
|
2026-02-15 13:04:12 +00:00
|
|
|
var toolCalls []ToolCall
|
|
|
|
|
|
|
|
|
|
for _, block := range resp.Content {
|
|
|
|
|
switch block.Type {
|
2026-03-05 01:51:18 +00:00
|
|
|
case "thinking":
|
|
|
|
|
tb := block.AsThinking()
|
|
|
|
|
reasoning.WriteString(tb.Thinking)
|
2026-02-15 13:04:12 +00:00
|
|
|
case "text":
|
|
|
|
|
tb := block.AsText()
|
2026-02-27 08:35:07 +00:00
|
|
|
content.WriteString(tb.Text)
|
2026-02-15 13:04:12 +00:00
|
|
|
case "tool_use":
|
|
|
|
|
tu := block.AsToolUse()
|
2026-02-18 19:48:23 +00:00
|
|
|
var args map[string]any
|
2026-02-15 13:04:12 +00:00
|
|
|
if err := json.Unmarshal(tu.Input, &args); err != nil {
|
2026-02-17 16:13:10 +00:00
|
|
|
log.Printf("anthropic: failed to decode tool call input for %q: %v", tu.Name, err)
|
2026-02-18 19:48:23 +00:00
|
|
|
args = map[string]any{"raw": string(tu.Input)}
|
2026-02-15 13:04:12 +00:00
|
|
|
}
|
|
|
|
|
toolCalls = append(toolCalls, ToolCall{
|
|
|
|
|
ID: tu.ID,
|
|
|
|
|
Name: tu.Name,
|
|
|
|
|
Arguments: args,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
finishReason := "stop"
|
|
|
|
|
switch resp.StopReason {
|
|
|
|
|
case anthropic.StopReasonToolUse:
|
|
|
|
|
finishReason = "tool_calls"
|
|
|
|
|
case anthropic.StopReasonMaxTokens:
|
|
|
|
|
finishReason = "length"
|
|
|
|
|
case anthropic.StopReasonEndTurn:
|
|
|
|
|
finishReason = "stop"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &LLMResponse{
|
2026-02-27 08:35:07 +00:00
|
|
|
Content: content.String(),
|
2026-03-05 01:51:18 +00:00
|
|
|
Reasoning: reasoning.String(),
|
2026-02-15 13:04:12 +00:00
|
|
|
ToolCalls: toolCalls,
|
|
|
|
|
FinishReason: finishReason,
|
|
|
|
|
Usage: &UsageInfo{
|
|
|
|
|
PromptTokens: int(resp.Usage.InputTokens),
|
|
|
|
|
CompletionTokens: int(resp.Usage.OutputTokens),
|
|
|
|
|
TotalTokens: int(resp.Usage.InputTokens + resp.Usage.OutputTokens),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|