Skip to content

Commit

Permalink
feat: support forefront
Browse files Browse the repository at this point in the history
  • Loading branch information
xiangsx committed May 5, 2023
1 parent 14eca0b commit ed590de
Show file tree
Hide file tree
Showing 9 changed files with 366 additions and 9 deletions.
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ This is a replication project for the typescript version of [gpt4free](https://g

The implemented models are:
- [x] you.com GPT-3.5 / Internet / good search
- [ ] forefront.ai GPT-4/3.5
- [x] forefront.ai GPT-4/3.5
- [ ] poe.com GPT-4/3.5
- [ ] writesonic.com GPT-3.5 / Internet
- [ ] t3nsor.com GPT-3.5
Expand Down Expand Up @@ -53,14 +53,19 @@ yarn start
docker-compose up --build -d
```

## test with curl
## Test with curl

common request
```shell
curl "http://127.0.0.1:3000/ask?prompt=你好"
curl "http://127.0.0.1:3000/ask?prompt=你好&model=forefront"
```

request event-stream
```shell
curl "http://127.0.0.1:3000/ask/stream?prompt=你好"
# test you
curl "http://127.0.0.1:3000/ask/stream?prompt=你好"

# test forefront
curl "http://127.0.0.1:3000/ask/stream?prompt=你好&model=forefront"
```
3 changes: 3 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ services:
# - TZ=Asia/Shanghai
- http_proxy=http://192.168.0.155:10811
- https_proxy=http://192.168.0.155:10811
# you should config this if you use forefront api, this apikey is used for receive register email
# get api key here https://rapidapi.com/Privatix/api/temp-mail
- rapid_api_key=xxxx
6 changes: 3 additions & 3 deletions model/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ export interface ChatOptions {

export interface Response {
text: string | null;
other: any;
other?: any;
}

export interface ResponseStream {
text: Stream;
other: any;
other?: any;
}

export interface Request {
Expand All @@ -29,7 +29,7 @@ export interface HistoryItem {
export abstract class Chat {
protected proxy: string | undefined;

constructor(options?: ChatOptions) {
protected constructor(options?: ChatOptions) {
this.proxy = options?.proxy;
}

Expand Down
175 changes: 175 additions & 0 deletions model/forefront/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//@ts-ignore
import UserAgent from 'user-agents';
import tlsClient from 'tls-client';

import {Chat, ChatOptions, Request, Response, ResponseStream} from "../base";
import {Email} from '../../utils/email';
import axios, {AxiosInstance, CreateAxiosDefaults} from "axios";
import {v4} from "uuid";
import es from "event-stream";
import {parseJSON} from "../../utils";

interface ForefrontRequest extends Request {
options?: {
chatId?: string;
prompt?: string;
actionType?: string;
defaultPersona?: string;
model?: string;
}
}

interface ChatCompletionChoice {
delta: {
content: string;
};
index: number;
finish_reason: string | null;
}

interface ChatCompletionChunk {
id: string;
object: string;
created: number;
model: string;
choices: ChatCompletionChoice[];
}

interface ForefrontSessionInfo {
agent: string;
token: string;
}

export class Forefront extends Chat {
private client: AxiosInstance | undefined;

constructor(options?: ChatOptions) {
super(options);
this.client = undefined;
}

public async ask(req: ForefrontRequest): Promise<Response> {
const res = await this.askStream(req);
let text = '';
return new Promise(resolve => {
res.text.pipe(es.split(/\r?\n\r?\n/)).pipe(es.map(async (chunk: any, cb: any) => {
const str = chunk.replace('data: ', '');
const data = parseJSON(str, {}) as ChatCompletionChunk;
if (!data.choices) {
cb(null, '');
return;
}
const [{delta: {content}}] = data.choices;
cb(null, content);
})).on('data', (data) => {
text += data;
}).on('close', () => {
resolve({text, other: res.other});
})
})

}

public async askStream(req: Request): Promise<ResponseStream> {
if (!this.client) {
await this.initClient();
}
if (!this.client) {
throw new Error('hava not created account');
}
const {
chatId = v4(),
actionType = 'new',
defaultPersona = '607e41fe-95be-497e-8e97-010a59b2e2c0',
model = 'gpt-4',
} = req.options || {};
const jsonData = {
text: req.prompt,
action: actionType,
parentId: chatId,
workspaceId: chatId,
messagePersona: defaultPersona,
model: model,
};

try {
const response = await this.client?.post(
'https://chat-server.tenant-forefront-default.knative.chi.coreweave.com/chat',
jsonData,
{responseType: 'stream'}
);
return {text: response.data};
} catch (e) {// session will expire very fast, I cannot know what reason
throw e;
}
}

async initClient() {
let hisSession = await this.createToken();
this.client = axios.create({
headers: {
'authority': 'chat-server.tenant-forefront-default.knative.chi.coreweave.com',
'accept': '*/*',
'accept-language': 'en,fr-FR;q=0.9,fr;q=0.8,es-ES;q=0.7,es;q=0.6,en-US;q=0.5,am;q=0.4,de;q=0.3',
'authorization': 'Bearer ' + hisSession.token,
'cache-control': 'no-cache',
'content-type': 'application/json',
'origin': 'https://chat.forefront.ai',
'pragma': 'no-cache',
'referer': 'https://chat.forefront.ai/',
'sec-ch-ua': '"Chromium";v="112", "Google Chrome";v="112", "Not:A-Brand";v="99"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'cross-site',
'user-agent': hisSession.agent,
}
} as CreateAxiosDefaults);
}

async createToken(): Promise<ForefrontSessionInfo> {
const mailbox = new Email();
const mailAddress = await mailbox.create();
const agent = new UserAgent().toString();
const session = new tlsClient.Session({clientIdentifier: 'chrome_108'});
session.headers = {
origin: 'https://accounts.forefront.ai',
'user-agent': agent, // Replace with actual random user agent
}
if (this.proxy) {
session.proxy = this.proxy;
}
const signEmailRes = await session.post('https://clerk.forefront.ai/v1/client/sign_ups?_clerk_js_version=4.38.4',
{data: {'email_address': mailAddress}});
const traceToken = (signEmailRes.data as any)?.response?.id;
if (!traceToken) {
throw new Error('Failed to create account! sign email res parse token failed!');
}

const verifyRes = await session.post(`https://clerk.forefront.ai/v1/client/sign_ups/${traceToken}/prepare_verification?_clerk_js_version=4.38.4`, {
data: {
'strategy': 'email_link',
'redirect_url': 'https://accounts.forefront.ai/sign-up/verify'
},
})
if (verifyRes.text.indexOf('sign_up_attempt') === -1) {
throw new Error('forefront create account failed');
}
const msgs = await mailbox.getMessage()
let validateURL: string | undefined;
for (const msg of msgs) {
validateURL = msg.mail_html.match(/https:\/\/clerk\.forefront\.ai\/v1\/verify\?token=[^\s"]+/i)?.[0];
if (validateURL) {
break;
}
}
if (!validateURL) {
throw new Error('Error while obtaining verfication URL!')
}
const validateRes = await session.get(validateURL)
const loginRes = await session.get('https://clerk.forefront.ai/v1/client?_clerk_js_version=4.38.4');
const token = (loginRes.data as any).response.sessions[0].last_active_token.jwt;
return {token, agent};
}
}
3 changes: 3 additions & 0 deletions model/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {Chat, ChatOptions} from "./base";
import {You} from "./you";
import {Forefront} from "./forefront";

export enum Model {
// define new model here
You = 'you',
Forefront = 'forefront',
}

export class ChatModelFactory {
Expand All @@ -19,6 +21,7 @@ export class ChatModelFactory {
init() {
// register new model here
this.modelMap.set(Model.You, new You(this.options))
this.modelMap.set(Model.Forefront, new Forefront(this.options))
}

get(model: Model): Chat | undefined {
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
"@types/koa-router": "^7.4.4",
"@types/node": "^18.16.3",
"@types/uuid": "^9.0.1",
"typescript": "^5.0.4",
"ts-node": "^10.9.1"
"@types/axios": "^0.14.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
},
"dependencies": {
"axios": "^1.4.0",
"event-stream": "^4.0.1",
"fake-useragent": "^1.0.1",
"https-proxy-agent": "^5.0.1",
Expand Down
99 changes: 99 additions & 0 deletions utils/email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import axios, {AxiosInstance, CreateAxiosDefaults} from 'axios';
import {md5, randomStr} from "./index";

export class Email {
private mail: TempMail;
private address: string | undefined;
private domainList: string[];
private mailID: string;

constructor() {
this.mail = new TempMail(process.env.rapid_api_key || "");
this.address = undefined;
this.mailID = '';
this.domainList = [];
}

async create(): Promise<any> {
this.domainList = await this.mail.getDomainsList();
this.address = `${randomStr()}${this.randomDomain()}`;
this.mailID = md5(this.address);
return this.address;
}

randomDomain(): string {
return this.domainList[Math.floor(Math.random() * this.domainList.length)];
}

emailAddress() {
if (!this.address) {
throw new Error('create first');
}
return this.address;
}

async getMessage() {
return this.mail.getEmail(this.mailID);
}
}

interface TempMailMessage {
_id: {
oid: string;
};
createdAt: {
milliseconds: number;
};
mail_id: string;
mail_address_id: string;
mail_from: string;
mail_subject: string;
mail_preview: string;
mail_text_only: string;
mail_text: string;
mail_html: string;
mail_timestamp: number;
mail_attachments_count: number;
mail_attachments: {
attachment: any[];
};
}

class TempMail {
private readonly client: AxiosInstance;

constructor(apikey: string) {
this.client = axios.create({
baseURL: 'https://privatix-temp-mail-v1.p.rapidapi.com/request/',
headers: {
'X-RapidAPI-Key': apikey,
'X-RapidAPI-Host': 'privatix-temp-mail-v1.p.rapidapi.com'
}
} as CreateAxiosDefaults);
}

async getEmail(md5Str: string): Promise<TempMailMessage[]> {
return new Promise(resolve => {
let time = 0;
const itl = setInterval(async () => {
const response = await this.client.get(`/mail/id/${md5Str}`);
if (response.data && response.data.length > 0) {
resolve(response.data);
clearInterval(itl);
return;
}
if (time > 5) {
resolve([]);
clearInterval(itl);
return;
}
time++;
}, 1000);
});
}

async getDomainsList(): Promise<string[]> {
const res = await this.client.get(`/domains/`);
return res.data;
}
}
Loading

0 comments on commit ed590de

Please sign in to comment.