Skip to content

Commit

Permalink
refactor: improve adapter architecture and type safety
Browse files Browse the repository at this point in the history
  • Loading branch information
liby committed Dec 6, 2024
1 parent e7ced3d commit 601e6d9
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 109 deletions.
2 changes: 1 addition & 1 deletion public/info.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"desc": "OpenAI: https://api.openai.com\n\nOpenAI Compatible: 完整的 API 地址,例如:https://gateway.ai.cloudflare.com/v1/CF_ACCOUNT_ID/GATEWAY_ID/openai/chat/completions\n\nAzure OpenAI: https://RESOURCE_NAME.openai.azure.com/openai/deployments/DEPLOYMENT_NAME/chat/completions?api-version=API_VERSION\n\nGoogle Gemini: https://generativelanguage.googleapis.com/v1beta/models",
"identifier": "apiUrl",
"textConfig": {
"height": "40",
"height": "55",
"placeholderText": "https://api.openai.com",
"type": "visible"
},
Expand Down
19 changes: 11 additions & 8 deletions src/adapter/azure-openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import { OpenAiChatCompletion } from "../types";
import { OpenAiAdapter } from "./openai";

export class AzureOpenAiAdapter extends OpenAiAdapter {

protected override troubleshootingLink = "https://bobtranslate.com/service/translate/azureopenai.html";
constructor() {
super({
troubleshootingLink: "https://bobtranslate.com/service/translate/azureopenai.html"
});
}

public override buildHeaders(apiKey: string): Record<string, string> {
return {
Expand All @@ -30,15 +33,15 @@ export class AzureOpenAiAdapter extends OpenAiAdapter {
type: isAuthError ? "secretKey" : "api",
message: errorData.message || "Unknown Azure OpenAI API error",
addition: errorData.code,
troubleshootingLink: this.troubleshootingLink
troubleshootingLink: this.config.troubleshootingLink
};
}

return {
type: "api",
message: "Azure OpenAI API error",
addition: JSON.stringify(response.data),
troubleshootingLink: this.troubleshootingLink
troubleshootingLink: this.config.troubleshootingLink
};
}

Expand All @@ -50,7 +53,7 @@ export class AzureOpenAiAdapter extends OpenAiAdapter {
const header = this.buildHeaders(apiKey);

try {
const resp = await $http.request({
const response = await $http.request({
method: "POST",
url: apiUrl,
header,
Expand All @@ -66,12 +69,12 @@ export class AzureOpenAiAdapter extends OpenAiAdapter {
}
});

if (resp.data.error) {
handleValidateError(completion, this.extractErrorFromResponse(resp));
if (response.data.error) {
handleValidateError(completion, this.extractErrorFromResponse(response));
return;
}

if ((resp.data as OpenAiChatCompletion).choices.length > 0) {
if ((response.data as OpenAiChatCompletion).choices.length > 0) {
completion({ result: true });
}
} catch (error) {
Expand Down
21 changes: 16 additions & 5 deletions src/adapter/base.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import { HttpResponse, ServiceError, TextTranslateQuery, ValidationCompletion } from "@bob-translate/types";
import { handleGeneralError, convertToServiceError } from "../utils";
import type { GeminiResponse, OpenAiChatCompletion, ServiceAdapter } from "../types";
import type { GeminiResponse, OpenAiChatCompletion, ServiceAdapter, ServiceAdapterConfig } from "../types";

export abstract class BaseAdapter implements ServiceAdapter {
protected constructor(protected readonly config: ServiceAdapterConfig) { }

protected getTemperature(): number {
return Number($option.temperature) ?? 0.2;
}

protected isStreamEnabled(): boolean {
return $option.stream === "enable";
}

protected getModel(): string {
return $option.model === "custom" ? $option.customModel : $option.model;
}

abstract buildHeaders(apiKey: string): Record<string, string>;

Expand All @@ -16,16 +29,14 @@ export abstract class BaseAdapter implements ServiceAdapter {

abstract testApiConnection(apiKey: string, apiUrl: string, completion: ValidationCompletion): Promise<void>;

protected abstract troubleshootingLink: string;

protected abstract extractErrorFromResponse(response: HttpResponse<any>): ServiceError;

protected handleInvalidToken(query: TextTranslateQuery) {
handleGeneralError(query, {
type: "secretKey",
message: "配置错误 - 请确保您在插件配置中填入了正确的 API Keys",
addition: "请在插件配置中填写正确的 API Keys",
troubleshootingLink: this.troubleshootingLink
troubleshootingLink: this.config.troubleshootingLink
});
}

Expand All @@ -52,7 +63,7 @@ export abstract class BaseAdapter implements ServiceAdapter {
protected handleRequestError(query: TextTranslateQuery, error: unknown) {
const serviceError = convertToServiceError(error);
if (serviceError.troubleshootingLink === undefined) {
serviceError.troubleshootingLink = this.troubleshootingLink;
serviceError.troubleshootingLink = this.config.troubleshootingLink;
}
handleGeneralError(query, serviceError);
}
Expand Down
26 changes: 12 additions & 14 deletions src/adapter/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { generatePrompts, handleValidateError } from "../utils";
import { BaseAdapter } from "./base";

export class GeminiAdapter extends BaseAdapter {

private model = $option.model === "custom" ? $option.customModel : $option.model;

private baseUrl = $option.apiUrl || 'https://generativelanguage.googleapis.com/v1beta/models';

protected troubleshootingLink = "https://bobtranslate.com/service/translate/gemini.html";
constructor() {
super({
troubleshootingLink: "https://bobtranslate.com/service/translate/gemini.html",
baseUrl: $option.apiUrl || 'https://generativelanguage.googleapis.com/v1beta/models'
});
}

protected extractErrorFromResponse(response: HttpResponse<any>): ServiceError {
const errorData = response.data?.error;
Expand All @@ -22,15 +22,15 @@ export class GeminiAdapter extends BaseAdapter {
type: isAuthError ? "secretKey" : "api",
message: errorData.message || "Unknown Gemini API error",
addition: errorData.status,
troubleshootingLink: this.troubleshootingLink
troubleshootingLink: this.config.troubleshootingLink
};
}

return {
type: "api",
message: "Gemini API error",
addition: JSON.stringify(response.data),
troubleshootingLink: this.troubleshootingLink
troubleshootingLink: this.config.troubleshootingLink
};
}

Expand All @@ -42,7 +42,6 @@ export class GeminiAdapter extends BaseAdapter {
}

public buildRequestBody(query: TextTranslateQuery): Record<string, unknown> {
const { temperature } = $option;
const { generatedSystemPrompt, generatedUserPrompt } = generatePrompts(query);

return {
Expand All @@ -57,7 +56,7 @@ export class GeminiAdapter extends BaseAdapter {
}
},
generationConfig: {
temperature: Number(temperature) ?? 0.2,
temperature: this.getTemperature(),
topK: 40,
topP: 0.95,
maxOutputTokens: 8192,
Expand All @@ -67,9 +66,8 @@ export class GeminiAdapter extends BaseAdapter {
}

public getTextGenerationUrl(_apiUrl: string): string {
const { stream } = $option;
const operationName = stream === "enable" ? 'streamGenerateContent' : 'generateContent';
return `${this.baseUrl}/${this.model}:${operationName}`;
const operationName = this.isStreamEnabled() ? 'streamGenerateContent' : 'generateContent';
return `${this.config.baseUrl}/${this.getModel()}:${operationName}`;
}

public parseResponse(response: HttpResponse<GeminiResponse | OpenAiChatCompletion>): string {
Expand Down Expand Up @@ -134,7 +132,7 @@ export class GeminiAdapter extends BaseAdapter {
},
});
}
} catch (error) {
} catch (_error) {
throw new Error('Failed to parse Gemini stream response');
}

Expand Down
2 changes: 1 addition & 1 deletion src/adapter/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { OpenAiAdapter } from './openai';
import { GeminiAdapter } from './gemini';
import { OpenAiCompatibleAdapter } from "./openai";
import type { ServiceAdapter, ServiceProvider } from '../types';
import { AzureOpenAiAdapter } from './azure-openai';
import { OpenAiCompatibleAdapter } from './openai-compatible';

export const getServiceAdapter = (serviceProvider: ServiceProvider): ServiceAdapter => {
switch (serviceProvider) {
Expand Down
11 changes: 11 additions & 0 deletions src/adapter/openai-compatible.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { OpenAiAdapter } from "./openai";

export class OpenAiCompatibleAdapter extends OpenAiAdapter {
public override getTextGenerationUrl(apiUrl: string): string {
return apiUrl;
}

protected override getValidationUrl(apiUrl: string): string {
return apiUrl.replace(/\/chat\/completions$/, '/models');
}
}
79 changes: 43 additions & 36 deletions src/adapter/openai.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,47 @@
import { HttpResponse, ServiceError, TextTranslateQuery, ValidationCompletion } from "@bob-translate/types";
import type { OpenAiChatCompletion, GeminiResponse, OpenAiModelList } from "../types";
import { generatePrompts, handleValidateError, isServiceError, replacePromptKeywords } from "../utils";
import type { OpenAiChatCompletion, GeminiResponse, OpenAiModelList, ServiceAdapterConfig, OpenAiErrorDetail, OpenAiErrorResponse } from "../types";
import { generatePrompts, handleValidateError, isServiceError, replacePromptKeywords, createTypeGuard } from "../utils";
import { BaseAdapter } from "./base";

export class OpenAiAdapter extends BaseAdapter {
const hasOpenAiErrorShape = createTypeGuard<OpenAiErrorResponse>({
error: {
type: 'object'
}
});

private baseUrl = $option.apiUrl || "https://api.openai.com";
const hasOpenAiErrorDetailShape = createTypeGuard<OpenAiErrorDetail>({
message: { type: 'string' },
code: { type: 'string' },
type: { type: 'string' },
param: { type: 'string', nullable: true }
});

export class OpenAiAdapter extends BaseAdapter {
private buffer = '';

private model = $option.model === "custom" ? $option.customModel : $option.model;

protected troubleshootingLink = "https://bobtranslate.com/service/translate/openai.html";
constructor(config?: ServiceAdapterConfig) {
super(config || {
troubleshootingLink: "https://bobtranslate.com/service/translate/openai.html",
baseUrl: $option.apiUrl || "https://api.openai.com"
});
}

protected extractErrorFromResponse(response: HttpResponse<any>): ServiceError {
const errorData = response.data?.error;
if (errorData) {
protected extractErrorFromResponse(errorResponse: HttpResponse<unknown>): ServiceError {
if (hasOpenAiErrorShape(errorResponse.data) && hasOpenAiErrorDetailShape(errorResponse.data.error)) {
const { error: errorDetail } = errorResponse.data;
return {
type: errorData.code === "invalid_api_key" ? "secretKey" : "api",
message: errorData.message || "Unknown OpenAI API error",
addition: errorData.type,
troubleshootingLink: this.troubleshootingLink
type: errorDetail.code === "invalid_api_key" ? "secretKey" : "api",
message: errorDetail.message || "Unknown OpenAI API error",
addition: errorDetail.type,
troubleshootingLink: this.config.troubleshootingLink
};
}

return {
type: "api",
message: "OpenAI API error",
addition: JSON.stringify(response.data),
troubleshootingLink: this.troubleshootingLink
message: errorResponse.response.statusCode === 401 ? "Invalid API key" : "OpenAI API error",
addition: JSON.stringify(errorResponse.data),
troubleshootingLink: this.config.troubleshootingLink
};
}

Expand All @@ -40,22 +53,20 @@ export class OpenAiAdapter extends BaseAdapter {
}

public buildRequestBody(query: TextTranslateQuery): Record<string, unknown> {
const { customSystemPrompt, customUserPrompt, temperature, stream } = $option;
const { customSystemPrompt, customUserPrompt } = $option;
const { generatedSystemPrompt, generatedUserPrompt } = generatePrompts(query);

const systemPrompt = replacePromptKeywords(customSystemPrompt, query) || generatedSystemPrompt;
const userPrompt = replacePromptKeywords(customUserPrompt, query) || generatedUserPrompt;

const modelTemperature = Number(temperature) ?? 0.2;

return {
model: this.model,
temperature: modelTemperature,
model: this.getModel(),
temperature: this.getTemperature(),
max_tokens: 1000,
top_p: 1,
frequency_penalty: 1,
presence_penalty: 1,
stream: stream === "enable",
stream: this.isStreamEnabled(),
messages: [
{
role: "system",
Expand Down Expand Up @@ -91,14 +102,14 @@ export class OpenAiAdapter extends BaseAdapter {
}

public getTextGenerationUrl(_apiUrl: string): string {
return `${this.baseUrl}/v1/chat/completions`;
return `${this.config.baseUrl}/v1/chat/completions`;
}

protected getValidationUrl(_apiUrl: string): string {
return `${this.baseUrl}/v1/models`;
return `${this.config.baseUrl}/v1/models`;
}

private parseStreamResponse(text: string): string | null {
protected parseStreamResponse(text: string): string | null {
if (text === '[DONE]') {
return null;
}
Expand Down Expand Up @@ -142,14 +153,14 @@ export class OpenAiAdapter extends BaseAdapter {
type: type || 'param',
message: message || 'Failed to parse JSON',
addition,
troubleshootingLink: this.troubleshootingLink
troubleshootingLink: this.config.troubleshootingLink
};
} else {
throw {
type: 'param',
message: 'An unknown error occurred',
addition: JSON.stringify(error),
troubleshootingLink: this.troubleshootingLink
troubleshootingLink: this.config.troubleshootingLink
};
}
}
Expand All @@ -171,18 +182,18 @@ export class OpenAiAdapter extends BaseAdapter {
const validationUrl = this.getValidationUrl(apiUrl);

try {
const resp = await $http.request({
const response = await $http.request({
method: "GET",
url: validationUrl,
header
});

if (resp.data.error) {
handleValidateError(completion, this.extractErrorFromResponse(resp));
if (hasOpenAiErrorShape(response.data)) {
handleValidateError(completion, this.extractErrorFromResponse(response));
return;
}

const modelList = resp.data as OpenAiModelList;
const modelList = response.data as OpenAiModelList;
if (modelList.data?.length > 0) {
completion({ result: true });
}
Expand All @@ -191,7 +202,3 @@ export class OpenAiAdapter extends BaseAdapter {
}
}
}

export class OpenAiCompatibleAdapter extends OpenAiAdapter {
protected override troubleshootingLink = "https://bobtranslate.com/service/translate/openai-compatible.html";
}
1 change: 0 additions & 1 deletion src/const.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { ensureHttpsAndNoTrailingSlash } from "./utils";
const validatePluginConfig = (): ServiceError | null => {
const { apiKeys, apiUrl, customModel, model, serviceProvider } = $option;

if (["openai-compatible", "azure-openai"].includes(serviceProvider) && !apiUrl) {
if (["azure-openai", "openai-compatible"].includes(serviceProvider) && !apiUrl) {
return {
type: "param",
message: "配置错误 - 请填写 API URL",
Expand Down
Loading

0 comments on commit 601e6d9

Please sign in to comment.