(ctx context.Context, messages []api.Message, tools []api.ToolDef)
| 73 | } |
| 74 | |
| 75 | func (p *OpenAIProvider) Send(ctx context.Context, messages []api.Message, tools []api.ToolDef) (api.Response, error) { |
| 76 | p.mu.Lock() |
| 77 | model := p.model |
| 78 | p.mu.Unlock() |
| 79 | |
| 80 | reqSummary := fmt.Sprintf("→ openai.Send model=%s msgs=%d tools=%d", model, len(messages), len(tools)) |
| 81 | reqID := debug.Recordp("provider", debug.LevelInfo, reqSummary, marshalRequestPayload(model, p.system, p.maxTokens, messages, tools)) |
| 82 | start := time.Now() |
| 83 | resp, err := p.client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{ |
| 84 | Model: shared.ChatModel(model), |
| 85 | Messages: p.toMessages(messages), |
| 86 | Tools: p.toTools(tools), |
| 87 | MaxCompletionTokens: param.NewOpt(p.maxTokens), |
| 88 | }) |
| 89 | elapsed := time.Since(start).Round(time.Millisecond) |
| 90 | if err != nil { |
| 91 | debug.Recordfc(reqID, "provider", debug.LevelError, "← openai.Send error (%v): %v", elapsed, err) |
| 92 | return api.Response{}, err |
| 93 | } |
| 94 | if len(resp.Choices) == 0 { |
| 95 | return api.Response{StopReason: api.StopOther}, nil |
| 96 | } |
| 97 | choice := resp.Choices[0] |
| 98 | |
| 99 | out := api.Response{StopReason: fromFinishReason(choice.FinishReason)} |
| 100 | if choice.Message.Content != "" { |
| 101 | out.Content = append(out.Content, api.Block{Type: api.BlockText, Text: choice.Message.Content}) |
| 102 | } |
| 103 | for _, tc := range choice.Message.ToolCalls { |
| 104 | out.Content = append(out.Content, api.Block{ |
| 105 | Type: api.BlockToolUse, |
| 106 | ToolUseID: tc.ID, |
| 107 | ToolName: tc.Function.Name, |
| 108 | ToolInput: tc.Function.Arguments, |
| 109 | }) |
| 110 | } |
| 111 | |
| 112 | out.Usage = api.Usage{ |
| 113 | InputTokens: int(resp.Usage.PromptTokens), |
| 114 | OutputTokens: int(resp.Usage.CompletionTokens), |
| 115 | CacheReadTokens: int(resp.Usage.PromptTokensDetails.CachedTokens), |
| 116 | // OpenAI doesn't expose cache-creation separately; cached_tokens is |
| 117 | // the only cache figure they return, and it maps to cache reads. |
| 118 | } |
| 119 | p.mu.Lock() |
| 120 | p.total = p.total.Add(out.Usage) |
| 121 | p.mu.Unlock() |
| 122 | respSummary := fmt.Sprintf("← openai.Send (%v in=%d out=%d cache=%d stop=%s)", |
| 123 | elapsed, out.Usage.InputTokens, out.Usage.OutputTokens, out.Usage.CacheReadTokens, out.StopReason) |
| 124 | debug.Recordpc(reqID, "provider", debug.LevelInfo, respSummary, marshalResponsePayload(out, elapsed)) |
| 125 | return out, nil |
| 126 | } |
| 127 | |
| 128 | // toMessages translates our generic Message slice into the OpenAI chat-message |
| 129 | // shape. Two notable splits: |
nothing calls this directly
no test coverage detected