mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
Perplexity api (#1432)
I have added Perplexity to the default AI models. I see Anthropic models are becoming part of the default as well, so I thought I should add a model that is specific for web search. This pull request is a work in progress; reviews and edit recommendations are welcome. --------- Co-authored-by: sawka <mike@commandline.dev>
This commit is contained in:
parent
b706d4524b
commit
c071cc04c3
@ -66,6 +66,23 @@ Set these keys:
|
|||||||
|
|
||||||
Note: we set the `ai:*` key to true to clear all the existing "ai" keys so this config is a clean slate.
|
Note: we set the `ai:*` key to true to clear all the existing "ai" keys so this config is a clean slate.
|
||||||
|
|
||||||
|
### How can I connect to Perplexity?
|
||||||
|
|
||||||
|
Open your [config file](./config) in Wave using `wsh editconfig`.
|
||||||
|
|
||||||
|
Set these keys:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ai:*": true,
|
||||||
|
"ai:apitype": "perplexity",
|
||||||
|
"ai:model": "llama-3.1-sonar-small-128k-online",
|
||||||
|
"ai:apitoken": "<your perplexity API key>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: we set the `ai:*` key to true to clear all the existing "ai" keys so this config is a clean slate.
|
||||||
|
|
||||||
To switch between models, consider [adding AI Presets](./presets) instead.
|
To switch between models, consider [adding AI Presets](./presets) instead.
|
||||||
|
|
||||||
### How can I see the block numbers?
|
### How can I see the block numbers?
|
||||||
|
@ -180,40 +180,54 @@ export class WaveAiModel implements ViewModel {
|
|||||||
const presetKey = get(this.presetKey);
|
const presetKey = get(this.presetKey);
|
||||||
const presetName = presets[presetKey]?.["display:name"] ?? "";
|
const presetName = presets[presetKey]?.["display:name"] ?? "";
|
||||||
const isCloud = isBlank(aiOpts.apitoken) && isBlank(aiOpts.baseurl);
|
const isCloud = isBlank(aiOpts.apitoken) && isBlank(aiOpts.baseurl);
|
||||||
if (aiOpts?.apitype == "anthropic") {
|
|
||||||
const modelName = aiOpts.model;
|
// Handle known API providers
|
||||||
viewTextChildren.push({
|
switch (aiOpts?.apitype) {
|
||||||
elemtype: "iconbutton",
|
case "anthropic":
|
||||||
icon: "globe",
|
|
||||||
title: "Using Remote Antropic API (" + modelName + ")",
|
|
||||||
noAction: true,
|
|
||||||
});
|
|
||||||
} else if (isCloud) {
|
|
||||||
viewTextChildren.push({
|
|
||||||
elemtype: "iconbutton",
|
|
||||||
icon: "cloud",
|
|
||||||
title: "Using Wave's AI Proxy (gpt-4o-mini)",
|
|
||||||
noAction: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const baseUrl = aiOpts.baseurl ?? "OpenAI Default Endpoint";
|
|
||||||
const modelName = aiOpts.model;
|
|
||||||
if (baseUrl.startsWith("http://localhost") || baseUrl.startsWith("http://127.0.0.1")) {
|
|
||||||
viewTextChildren.push({
|
|
||||||
elemtype: "iconbutton",
|
|
||||||
icon: "location-dot",
|
|
||||||
title: "Using Local Model @ " + baseUrl + " (" + modelName + ")",
|
|
||||||
noAction: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
viewTextChildren.push({
|
viewTextChildren.push({
|
||||||
elemtype: "iconbutton",
|
elemtype: "iconbutton",
|
||||||
icon: "globe",
|
icon: "globe",
|
||||||
title: "Using Remote Model @ " + baseUrl + " (" + modelName + ")",
|
title: `Using Remote Anthropic API (${aiOpts.model})`,
|
||||||
noAction: true,
|
noAction: true,
|
||||||
});
|
});
|
||||||
}
|
break;
|
||||||
|
case "perplexity":
|
||||||
|
viewTextChildren.push({
|
||||||
|
elemtype: "iconbutton",
|
||||||
|
icon: "globe",
|
||||||
|
title: `Using Remote Perplexity API (${aiOpts.model})`,
|
||||||
|
noAction: true,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (isCloud) {
|
||||||
|
viewTextChildren.push({
|
||||||
|
elemtype: "iconbutton",
|
||||||
|
icon: "cloud",
|
||||||
|
title: "Using Wave's AI Proxy (gpt-4o-mini)",
|
||||||
|
noAction: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const baseUrl = aiOpts.baseurl ?? "OpenAI Default Endpoint";
|
||||||
|
const modelName = aiOpts.model;
|
||||||
|
if (baseUrl.startsWith("http://localhost") || baseUrl.startsWith("http://127.0.0.1")) {
|
||||||
|
viewTextChildren.push({
|
||||||
|
elemtype: "iconbutton",
|
||||||
|
icon: "location-dot",
|
||||||
|
title: `Using Local Model @ ${baseUrl} (${modelName})`,
|
||||||
|
noAction: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
viewTextChildren.push({
|
||||||
|
elemtype: "iconbutton",
|
||||||
|
icon: "globe",
|
||||||
|
title: `Using Remote Model @ ${baseUrl} (${modelName})`,
|
||||||
|
noAction: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dropdownItems = Object.entries(presets)
|
const dropdownItems = Object.entries(presets)
|
||||||
.sort((a, b) => ((a[1]["display:order"] ?? 0) > (b[1]["display:order"] ?? 0) ? 1 : -1))
|
.sort((a, b) => ((a[1]["display:order"] ?? 0) > (b[1]["display:order"] ?? 0) ? 1 : -1))
|
||||||
.map(
|
.map(
|
||||||
|
179
pkg/waveai/perplexitybackend.go
Normal file
179
pkg/waveai/perplexitybackend.go
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package waveai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/panichandler"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PerplexityBackend struct{}
|
||||||
|
|
||||||
|
var _ AIBackend = PerplexityBackend{}
|
||||||
|
|
||||||
|
// Perplexity API request types
|
||||||
|
type perplexityMessage struct {
|
||||||
|
Role string `json:"role"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type perplexityRequest struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Messages []perplexityMessage `json:"messages"`
|
||||||
|
Stream bool `json:"stream"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perplexity API response types
|
||||||
|
type perplexityResponseDelta struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type perplexityResponseChoice struct {
|
||||||
|
Delta perplexityResponseDelta `json:"delta"`
|
||||||
|
FinishReason string `json:"finish_reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type perplexityResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Choices []perplexityResponseChoice `json:"choices"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PerplexityBackend) StreamCompletion(ctx context.Context, request wshrpc.OpenAiStreamRequest) chan wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType] {
|
||||||
|
rtn := make(chan wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType])
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
panicErr := panichandler.PanicHandler("PerplexityBackend.StreamCompletion")
|
||||||
|
if panicErr != nil {
|
||||||
|
rtn <- makeAIError(panicErr)
|
||||||
|
}
|
||||||
|
close(rtn)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if request.Opts == nil {
|
||||||
|
rtn <- makeAIError(errors.New("no perplexity opts found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
model := request.Opts.Model
|
||||||
|
if model == "" {
|
||||||
|
model = "llama-3.1-sonar-small-128k-online"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert messages format
|
||||||
|
var messages []perplexityMessage
|
||||||
|
for _, msg := range request.Prompt {
|
||||||
|
role := "user"
|
||||||
|
if msg.Role == "assistant" {
|
||||||
|
role = "assistant"
|
||||||
|
} else if msg.Role == "system" {
|
||||||
|
role = "system"
|
||||||
|
}
|
||||||
|
|
||||||
|
messages = append(messages, perplexityMessage{
|
||||||
|
Role: role,
|
||||||
|
Content: msg.Content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
perplexityReq := perplexityRequest{
|
||||||
|
Model: model,
|
||||||
|
Messages: messages,
|
||||||
|
Stream: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
reqBody, err := json.Marshal(perplexityReq)
|
||||||
|
if err != nil {
|
||||||
|
rtn <- makeAIError(fmt.Errorf("failed to marshal perplexity request: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.perplexity.ai/chat/completions", strings.NewReader(string(reqBody)))
|
||||||
|
if err != nil {
|
||||||
|
rtn <- makeAIError(fmt.Errorf("failed to create perplexity request: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+request.Opts.APIToken)
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
rtn <- makeAIError(fmt.Errorf("failed to send perplexity request: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
rtn <- makeAIError(fmt.Errorf("Perplexity API error: %s - %s", resp.Status, string(bodyBytes)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := bufio.NewReader(resp.Body)
|
||||||
|
sentHeader := false
|
||||||
|
|
||||||
|
for {
|
||||||
|
// Check for context cancellation
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
rtn <- makeAIError(fmt.Errorf("request cancelled: %v", ctx.Err()))
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
rtn <- makeAIError(fmt.Errorf("error reading stream: %v", err))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if !strings.HasPrefix(line, "data: ") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
data := strings.TrimPrefix(line, "data: ")
|
||||||
|
if data == "[DONE]" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var response perplexityResponse
|
||||||
|
if err := json.Unmarshal([]byte(data), &response); err != nil {
|
||||||
|
rtn <- makeAIError(fmt.Errorf("error unmarshaling response: %v", err))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sentHeader {
|
||||||
|
pk := MakeOpenAIPacket()
|
||||||
|
pk.Model = response.Model
|
||||||
|
rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Response: *pk}
|
||||||
|
sentHeader = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, choice := range response.Choices {
|
||||||
|
pk := MakeOpenAIPacket()
|
||||||
|
pk.Text = choice.Delta.Content
|
||||||
|
pk.FinishReason = choice.FinishReason
|
||||||
|
rtn <- wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType]{Response: *pk}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return rtn
|
||||||
|
}
|
@ -17,6 +17,7 @@ const OpenAICloudReqStr = "openai-cloudreq"
|
|||||||
const PacketEOFStr = "EOF"
|
const PacketEOFStr = "EOF"
|
||||||
const DefaultAzureAPIVersion = "2023-05-15"
|
const DefaultAzureAPIVersion = "2023-05-15"
|
||||||
const ApiType_Anthropic = "anthropic"
|
const ApiType_Anthropic = "anthropic"
|
||||||
|
const ApiType_Perplexity = "perplexity"
|
||||||
|
|
||||||
type OpenAICmdInfoPacketOutputType struct {
|
type OpenAICmdInfoPacketOutputType struct {
|
||||||
Model string `json:"model,omitempty"`
|
Model string `json:"model,omitempty"`
|
||||||
@ -74,6 +75,15 @@ func RunAICommand(ctx context.Context, request wshrpc.OpenAiStreamRequest) chan
|
|||||||
anthropicBackend := AnthropicBackend{}
|
anthropicBackend := AnthropicBackend{}
|
||||||
return anthropicBackend.StreamCompletion(ctx, request)
|
return anthropicBackend.StreamCompletion(ctx, request)
|
||||||
}
|
}
|
||||||
|
if request.Opts.APIType == ApiType_Perplexity {
|
||||||
|
endpoint := request.Opts.BaseURL
|
||||||
|
if endpoint == "" {
|
||||||
|
endpoint = "default"
|
||||||
|
}
|
||||||
|
log.Printf("sending ai chat message to perplexity endpoint %q using model %s\n", endpoint, request.Opts.Model)
|
||||||
|
perplexityBackend := PerplexityBackend{}
|
||||||
|
return perplexityBackend.StreamCompletion(ctx, request)
|
||||||
|
}
|
||||||
if IsCloudAIRequest(request.Opts) {
|
if IsCloudAIRequest(request.Opts) {
|
||||||
log.Print("sending ai chat message to default waveterm cloud endpoint\n")
|
log.Print("sending ai chat message to default waveterm cloud endpoint\n")
|
||||||
cloudBackend := WaveAICloudBackend{}
|
cloudBackend := WaveAICloudBackend{}
|
||||||
|
Loading…
Reference in New Issue
Block a user