Skip to content

Commit

Permalink
Merge pull request #19 from ilyydy/azure
Browse files Browse the repository at this point in the history
Azure
  • Loading branch information
ilyydy committed Apr 10, 2023
2 parents ce21abc + ea8eadf commit bdae71a
Show file tree
Hide file tree
Showing 13 changed files with 372 additions and 45 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# [0.3.0](https://github.com/ilyydy/cf-openai/compare/v0.2.1...v0.3.0) (2023-04-10)


### Features

* 支持 azure openai ([0e6fd4b](https://github.com/ilyydy/cf-openai/commit/0e6fd4b0e9119a8fef621bd8f915badcc022085e))



## [0.2.1](https://github.com/ilyydy/cf-openai/compare/v0.2.0...v0.2.1) (2023-04-08)


Expand Down
52 changes: 30 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![CodeQL](https://github.com/ilyydy/cf-openai/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/ilyydy/cf-openai/actions/workflows/github-code-scanning/codeql)
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)

基于 Cloudflare Worker 代理访问 OpenAI API 的服务,目前支持企业微信应用、微信公众号接入
基于 Cloudflare Worker 代理访问 [OpenAI](https://platform.openai.com/docs/api-reference)/[AzureOpenAI](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/) API 的服务,目前支持企业微信应用、微信公众号接入

- [cf-openai](#cf-openai)
- [基本要求](#基本要求)
Expand All @@ -22,7 +22,7 @@

## 基本要求

- 注册 OpenAI 账号,创建复制 API key
- 注册 OpenAI 账号,创建复制 API key,或注册 Azure 账号,创建资源并部署模型
- 注册 Cloudflare 账号,关于 [免费用量](<https://developers.cloudflare.com/workers/platform/limits/>)
- 国内服务接入还需要一个国内直接能访问的域名

Expand Down Expand Up @@ -102,26 +102,29 @@

输入使用时可忽略大小写

| 命令 | 可用角色 | 说明 |
| ----------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| /help | 游客,用户 | 获取命令帮助信息 |
| /bindKey | 游客,用户 | 绑定 OpenAI api key,格式如 /bindKey xxx。如已绑定 key,则会覆盖。绑定后先用 /testKey 命令测试是否正常可用 |
| /unbindKey | 用户 | 解绑 OpenAI api key |
| /testKey | 用户 | 调用 OpenAI 列出模型接口,测试 api key 是否正常绑定可用,不消耗用量 |
| /setChatType | 用户,试用者 | 切换对话模式,可选'单聊'和'串聊',默认'单聊'。'单聊'只处理当前的输入,'串聊'会带上历史聊天记录请求 OpenAI,消耗更多用量 |
| /newChat | 用户,试用者 | 清除之前的串聊历史记录,开始新的串聊 |
| /retry | 用户,试用者 | 根据 msgId 获取对应回答,回答只会保留 3 分钟。保留时间可通过 ANSWER_EXPIRES_MINUTES 配置 |
| .. | 用户,试用者 | 重试上一个延迟的回答 |
| 。。 | 用户,试用者 | 重试上一个延迟的回答 |
| /bindSessionKey | 游客,用户 | 绑定 OpenAI session key,可查看用量页面对 <https://api.openai.com/v1/usage> 的请求头获得,每次重新登陆原来的 session key 会失效,需要重新绑定 |
| /unbindSessionKey | 用户 | 解绑 OpenAI session key |
| /usage | 用户 | 获取本月用量信息,可能有 5 分钟左右的延迟,需要绑定 OpenAI api key 或 session key |
| /freeUsage | 用户 | 获取免费用量信息,可能有 5 分钟左右的延迟,需要绑定 OpenAI session key |
| /system | 用户,管理员 | 查看当前一些系统配置信息,如当前 OpenAI 模型,当前用户 ID 等 |
| /faq | 游客,用户 | 一些常见问题 |
| /adminAuth | 游客,用户 | 通过 token 认证成为管理员,避免每个平台配置 admin 用户 ID 的麻烦。需要先配置 ADMIN_AUTH_TOKEN |
| /testAlarm | 管理员 | 测试发送告警消息。需要先配置 ALARM_URL |
| /feedback | 游客,用户 | 用户向开发者发送反馈。需要先配置 FEEDBACK_URL |
| 命令 | 可用角色 | 说明 |
| ----------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| /help | 游客,用户 | 获取命令帮助信息 |
| /setOpenAiType | 游客,用户 | 设置使用 openAi 还是 azureOpenAi,默认使用 openAi |
| /bindKey | 游客,用户 | 绑定 OpenAI api key,格式如 /bindKey xxx。如已绑定 key,则会覆盖。绑定后先用 /testKey 命令测试是否正常可用 |
| /unbindKey | 用户 | 解绑 OpenAI api key |
| /bindAzureKey | 游客,用户 | 绑定 AzureOpenAI key,格式如 /bindAzureKey yourResourceName:yourDeploymentName:yourApiKey。如已绑定 key,则会覆盖。绑定后先用 /testKey 命令测试是否正常可用 |
| /unbindAzureKey | 用户 | 解绑 AzureOpenAI api key |
| /testKey | 用户 | 调用 OpenAI/AzureOpenAI 列出模型接口,测试 api key 是否正常绑定可用,不消耗用量 |
| /setChatType | 用户,试用者 | 切换对话模式,可选'单聊'和'串聊',默认'单聊'。'单聊'只处理当前的输入,'串聊'会带上历史聊天记录请求 OpenAI,消耗更多用量 |
| /newChat | 用户,试用者 | 清除之前的串聊历史记录,开始新的串聊 |
| /retry | 用户,试用者 | 根据 msgId 获取对应回答,回答只会保留 3 分钟。保留时间可通过 ANSWER_EXPIRES_MINUTES 配置 |
| .. | 用户,试用者 | 重试上一个延迟的回答 |
| 。。 | 用户,试用者 | 重试上一个延迟的回答 |
| /bindSessionKey | 游客,用户 | 绑定 OpenAI session key,可查看用量页面对 <https://api.openai.com/v1/usage> 的请求头获得,每次重新登陆原来的 session key 会失效,需要重新绑定 |
| /unbindSessionKey | 用户 | 解绑 OpenAI session key |
| /usage | 用户 | 获取本月用量信息,可能有 5 分钟左右的延迟,需要绑定 OpenAI api key 或 session key |
| /freeUsage | 用户 | 获取免费用量信息,可能有 5 分钟左右的延迟,需要绑定 OpenAI session key |
| /system | 用户,管理员 | 查看当前一些系统配置信息,如当前 OpenAI 模型,当前用户 ID 等 |
| /faq | 游客,用户 | 一些常见问题 |
| /adminAuth | 游客,用户 | 通过 token 认证成为管理员,避免每个平台配置 admin 用户 ID 的麻烦。需要先配置 ADMIN_AUTH_TOKEN |
| /testAlarm | 管理员 | 测试发送告警消息。需要先配置 ALARM_URL |
| /feedback | 游客,用户 | 用户向开发者发送反馈。需要先配置 FEEDBACK_URL |

## OpenAI 配置

Expand All @@ -141,6 +144,11 @@
| ANSWER_EXPIRES_MINUTES | 3 | 提问/回答的保存时长,分钟 |
| SYSTEM_INIT_MESSAGE | You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible. Knowledge cutoff: 2021-09-01. Current is 2023 | 发给 OpenAI 的默认第一条系统消息,可用于调整模型 |
| WELCOME_MESSAGE | 欢迎使用,可输入 /help 查看当前可用命令 | 用户关注应用时发出的欢迎信息 |
| AZURE_API_PREFIX | `https://RESOURCENAME.openai.azure.com/openai` | Azure OpenAI 通过请求前缀 |
| AZURE_CHAT_API_VERSION | 2023-03-15-preview | 聊天接口 API 版本 |
| AZURE_LIST_MODEL_API_VERSION | 2022-12-01 | 列举模型接口 API 版本 |
| AZURE_GUEST_KEY | | 可选,游客的默认 azure openai key,可被随意使用,跨平台起效,谨慎配置! |
| AZURE_ADMIN_KEY | | 可选,admin 用户的默认 azure openai key,跨平台起效 |

## 全局配置

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cf-openai",
"version": "0.2.1",
"version": "0.3.0",
"devDependencies": {
"@cloudflare/workers-types": "^4.20230307.0",
"@commitlint/cli": "^17.5.0",
Expand Down
170 changes: 170 additions & 0 deletions src/controller/openai/azureOpenAiClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { CONFIG } from './config'
import { errorToString, genFail, genSuccess, sleep } from '../../utils'
import * as kv from './kv'

import type openai from 'openai'
import type { Logger } from '../../utils'

const MODULE = 'src/controller/openai/azureOpenAiClient.ts'

export const finishReasonMap: { [key: string]: string } = {
length: '长度限制',
content_filter: '内容过滤',
}

export const defaultCompletionConfig: Omit<openai.CreateCompletionRequest, 'model'> = {
max_tokens: CONFIG.MAX_CHAT_TOKEN_NUM,
}

export const defaultChatCompletionConfig: Omit<openai.CreateChatCompletionRequest, 'messages' | 'model'> = {
max_tokens: CONFIG.MAX_CHAT_TOKEN_NUM,
...CONFIG.OPEN_AI_API_CHAT_EXTRA_PARAMS,
}

export class AzureOpenAiClient {
readonly basePath: string
readonly resourceName: string
readonly deployName: string
readonly apiKey: string

constructor(readonly key: string, readonly logger: Logger) {
const [resourceName, deployName, apiKey] = key.split(':')
this.resourceName = resourceName
this.deployName = deployName
this.apiKey = apiKey
this.basePath = CONFIG.AZURE_API_PREFIX.replace('RESOURCENAME', resourceName)
}

async base<T = any>(params: {
basePath?: string
extraPath?: string
init?: RequestInit<RequestInitCfProperties> & { timeout?: number }
}) {
const { extraPath = '', init = {} } = params

if (CONFIG.OPEN_AI_API_KEY_OCCUPYING_DURATION > 0) {
await this.waitToHoldApiKey(this.apiKey)
}

const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), init.timeout || CONFIG.OPEN_AI_API_TIMEOUT_MS)

const start = Date.now()
try {
const resp = await fetch(`${this.basePath}${extraPath}`, {
headers: {
'Content-Type': 'application/json',
'api-key': this.apiKey,
},
...init,
signal: controller.signal,
})

const json = await resp.json<Record<string, any>>()
this.logger.debug(`${MODULE} AzureOpenAI 回复 ${Date.now() - start} ${JSON.stringify(json)}`)

if ('error' in json && json.error) {
this.logger.error(`${MODULE} AzureOpenAI 错误 ${JSON.stringify(json.error)}`)
return genFail(`AzureOpenAI 错误\n> ${(json.error as Error).message}`)
}

return genSuccess(json as T)
} catch (e) {
const err = e as Error
this.logger.error(`${MODULE} 请求 AzureOpenAI 异常 ${Date.now() - start} ${errorToString(err)}`)
return genFail(`请求 AzureOpenAI 异常\n> ${err.name === 'AbortError' ? '请求超时' : err.message}`)
} finally {
clearTimeout(timer)
}
}

async listModels() {
const res = await this.base<openai.ListEnginesResponse>({
extraPath: `/models?api-version=${CONFIG.AZURE_LIST_MODEL_API_VERSION}`,
init: {
method: 'GET',
timeout: 3000,
},
})
if (!res.success) return res

const { data } = res.data
return genSuccess(data)
}

async createCompletion(prompt: openai.CreateCompletionRequestPrompt, config = defaultCompletionConfig) {
const res = await this.base<openai.CreateCompletionResponse>({
extraPath: `/deployments/${this.deployName}/completions?api-version=${CONFIG.AZURE_CHAT_API_VERSION}`,
init: {
method: 'POST',
body: JSON.stringify({
prompt,
...config,
}),
},
})
if (!res.success) return res

const { id, usage, choices } = res.data
const first = choices[0]
if (first && first.text) {
return genSuccess({
id,
usage,
msg: first.text,
finishReason: first.finish_reason ?? 'unknown',
finishReasonZh: finishReasonMap[first.finish_reason as string] ?? '未知',
})
}

return genFail(`AzureOpenAI 返回异常\n> 数据为空`)
}

async createChatCompletion(messages: openai.ChatCompletionRequestMessage[], config = defaultChatCompletionConfig) {
const res = await this.base<openai.CreateChatCompletionResponse>({
extraPath: `/deployments/${this.deployName}/chat/completions?api-version=${CONFIG.AZURE_CHAT_API_VERSION}`,
init: {
method: 'POST',
body: JSON.stringify({
messages,
...config,
}),
},
})
if (!res.success) return res

const { id, usage, choices } = res.data
const first = choices[0]
if (first && first.message) {
return genSuccess({
id,
usage: usage as openai.CreateCompletionResponseUsage,
msg: first.message,
finishReason: first.finish_reason || 'unknown',
finishReasonZh: finishReasonMap[first.finish_reason as string] ?? '未知',
})
}

return genFail(`AzureOpenAI 返回异常\n> 数据为空`)
}

/**
* 用 kv 实现限流只能说勉强能用
* TODO @see https://github.com/ilyydy/cf-openai/issues/14
*/
async waitToHoldApiKey(apiKey: string) {
const waitDurationRes = await kv.getApiKeyWaitDuration(apiKey)
if (!waitDurationRes.success) {
this.logger.error(`${MODULE} 获取 apiKey '${apiKey}' 的过期时间失败`)
return '服务异常'
}

const waitDuration = waitDurationRes.data
this.logger.debug(`${MODULE} apiKey '${apiKey}' waitDuration ${waitDuration}ms`)

await kv.setApiKeyOccupied(apiKey, CONFIG.OPEN_AI_API_KEY_OCCUPYING_DURATION * 1000 + waitDuration + 1000)
if (waitDuration > 0) {
await sleep(waitDuration)
}
}
}
9 changes: 9 additions & 0 deletions src/controller/openai/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import type { OpenAiConfig } from '../../types'

export const commandName = {
help: '/help',
setOpenAiType: '/setOpenAiType',
bindKey: '/bindKey',
unbindKey: '/unbindKey',
bindAzureKey: '/bindAzureKey',
unbindAzureKey: '/unbindAzureKey',
testKey: '/testKey',
setChatType: '/setChatType',
newChat: '/newChat',
Expand Down Expand Up @@ -57,4 +60,10 @@ export const CONFIG: OpenAiConfig = {
SYSTEM_INIT_MESSAGE: `You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible. Knowledge cutoff: 2021-09-01. Current is 2023`,
// 用户关注应用时发出的欢迎信息
WELCOME_MESSAGE: `欢迎使用,可输入 ${commandName.help} 查看当前可用命令`,

AZURE_API_PREFIX: 'https://RESOURCENAME.openai.azure.com/openai',
AZURE_CHAT_API_VERSION: '2023-03-15-preview',
AZURE_LIST_MODEL_API_VERSION: '2022-12-01',
AZURE_GUEST_KEY: '',
AZURE_ADMIN_KEY: '',
}
Loading

0 comments on commit bdae71a

Please sign in to comment.