mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-05 04:07:36 +08:00
feat: plugin system
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
import type {
|
||||
ProviderInvokeInput,
|
||||
ProviderOutput,
|
||||
ProviderResult,
|
||||
ProviderSupport,
|
||||
} from "cursor-byok:provider";
|
||||
import type { PluginContext } from "cursor-byok:plugin";
|
||||
import { HttpError, streamOpenAiResponses } from "cursor-byok:protocol/openai-responses";
|
||||
import { codexModels, reasoningEfforts } from "./models.ts";
|
||||
import {
|
||||
type AccountData,
|
||||
accountData,
|
||||
chatGptAccountId,
|
||||
quotaExhaustedPatch,
|
||||
RESOURCE_TYPE,
|
||||
} from "./resources.ts";
|
||||
|
||||
const RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
||||
|
||||
/** 流内错误只有文本可用,按额度关键词分类。 */
|
||||
export function isQuotaError(error: string): boolean {
|
||||
const message = error.toLowerCase();
|
||||
return message.includes("insufficient_quota") ||
|
||||
message.includes("usage_limit_reached") ||
|
||||
message.includes("exceeded your current quota") ||
|
||||
message.includes("quota_exceeded") ||
|
||||
message.includes("5-hour") ||
|
||||
message.includes("5 hour") ||
|
||||
(message.includes("429") &&
|
||||
(message.includes("quota") || message.includes("usage_limit") ||
|
||||
message.includes("insufficient")));
|
||||
}
|
||||
|
||||
/** HTTP 失败携带结构化状态码,429 时放宽响应体的匹配条件。 */
|
||||
function isQuotaHttpError(error: HttpError): boolean {
|
||||
const body = error.body.toLowerCase();
|
||||
return body.includes("insufficient_quota") ||
|
||||
body.includes("usage_limit_reached") ||
|
||||
body.includes("exceeded your current quota") ||
|
||||
body.includes("quota_exceeded") ||
|
||||
body.includes("5-hour") ||
|
||||
body.includes("5 hour") ||
|
||||
(error.status === 429 &&
|
||||
(body.includes("quota") || body.includes("usage_limit") || body.includes("insufficient")));
|
||||
}
|
||||
|
||||
function invalidResult(message: string, stateMessage: string): ProviderResult {
|
||||
return {
|
||||
status: "resource-error",
|
||||
message,
|
||||
patch: { state: { status: "invalid", message: stateMessage } },
|
||||
};
|
||||
}
|
||||
|
||||
function headers(data: AccountData, cacheKey: string | null): Record<string, string> {
|
||||
const result: Record<string, string> = {
|
||||
authorization: `Bearer ${data.accessToken}`,
|
||||
originator: "codex_cli_rs",
|
||||
};
|
||||
const accountId = chatGptAccountId(data.accessToken);
|
||||
if (accountId) result["ChatGPT-Account-Id"] = accountId;
|
||||
// Codex 后端的缓存亲和契约:session-id / thread-id / prompt_cache_key
|
||||
// 三者同源(见 codex-rs client.rs);缺头会导致请求落在随机分片上。
|
||||
if (cacheKey !== null) {
|
||||
result["session-id"] = cacheKey;
|
||||
result["thread-id"] = cacheKey;
|
||||
result["x-client-request-id"] = cacheKey;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function invoke(
|
||||
input: ProviderInvokeInput,
|
||||
output: ProviderOutput,
|
||||
context: PluginContext,
|
||||
): Promise<ProviderResult> {
|
||||
if (!input.resource) {
|
||||
return { status: "request-error", message: "add a ChatGPT account before calling Codex" };
|
||||
}
|
||||
let data: AccountData;
|
||||
try {
|
||||
data = accountData(input.resource);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return invalidResult(message, message);
|
||||
}
|
||||
const efforts = reasoningEfforts(input.model);
|
||||
const reasoning = input.request.reasoning;
|
||||
const effort = reasoning.effort !== null && efforts.includes(reasoning.effort)
|
||||
? reasoning.effort
|
||||
: null;
|
||||
try {
|
||||
await streamOpenAiResponses(
|
||||
{
|
||||
url: RESPONSES_URL,
|
||||
model: input.model.id,
|
||||
// Codex 订阅端点不接受 max_output_tokens;fast 档位经协议库映射为
|
||||
// service_tier: "priority" 后透传。
|
||||
request: {
|
||||
...input.request,
|
||||
reasoning: { enabled: reasoning.enabled, effort },
|
||||
maxOutputTokens: null,
|
||||
},
|
||||
headers: headers(data, input.request.cacheKey),
|
||||
extraBody: { store: false },
|
||||
},
|
||||
output,
|
||||
context,
|
||||
);
|
||||
return { status: "completed" };
|
||||
} catch (error) {
|
||||
if (error instanceof HttpError) {
|
||||
if (error.status === 401) {
|
||||
return invalidResult(error.message, "ChatGPT authorization expired; sign in again");
|
||||
}
|
||||
if (isQuotaHttpError(error)) {
|
||||
return {
|
||||
status: "resource-error",
|
||||
message: error.message,
|
||||
patch: quotaExhaustedPatch(data, error.body),
|
||||
};
|
||||
}
|
||||
return { status: "request-error", message: error.message };
|
||||
}
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (isQuotaError(message)) {
|
||||
return { status: "resource-error", message, patch: quotaExhaustedPatch(data, message) };
|
||||
}
|
||||
return { status: "request-error", message };
|
||||
}
|
||||
}
|
||||
|
||||
export const codexProvider: ProviderSupport = {
|
||||
id: "codex",
|
||||
displayName: "OpenAI Codex",
|
||||
description: {
|
||||
"en-US": "ChatGPT subscription access through the official Codex Responses API.",
|
||||
"zh-CN": "通过官方 Codex Responses API 使用 ChatGPT 订阅。",
|
||||
},
|
||||
providerType: "openai",
|
||||
resourceType: RESOURCE_TYPE,
|
||||
models: codexModels,
|
||||
invoke,
|
||||
};
|
||||
Reference in New Issue
Block a user