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,129 @@
|
||||
import type { JsonValue } from "cursor-byok:plugin";
|
||||
import type { ModelDefinition, ModelSnapshot, ModelSupport } from "cursor-byok:model";
|
||||
import { accountData, accountHeaders } from "./resources.ts";
|
||||
|
||||
const MODELS_URL = "https://chatgpt.com/backend-api/codex/models?client_version=1.0.0";
|
||||
|
||||
function object(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
}
|
||||
|
||||
function text(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : null;
|
||||
}
|
||||
|
||||
function positiveInteger(value: unknown): number | null {
|
||||
const parsed = typeof value === "number"
|
||||
? value
|
||||
: typeof value === "string"
|
||||
? Number(value)
|
||||
: NaN;
|
||||
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : null;
|
||||
}
|
||||
|
||||
function parseReasoningEfforts(model: Record<string, unknown>): string[] {
|
||||
const source = model.supported_reasoning_efforts ??
|
||||
model.supportedReasoningEfforts ??
|
||||
model.reasoning_efforts ??
|
||||
model.reasoningEfforts;
|
||||
if (!Array.isArray(source)) return [];
|
||||
const values = source.flatMap((item) => {
|
||||
if (typeof item === "string") return [item.trim()];
|
||||
const entry = object(item);
|
||||
const value = text(entry?.effort ?? entry?.id ?? entry?.value ?? entry?.name);
|
||||
return value ? [value] : [];
|
||||
}).filter(Boolean);
|
||||
return [...new Set(values)];
|
||||
}
|
||||
|
||||
function modelId(value: unknown): string | null {
|
||||
if (typeof value === "string") return text(value);
|
||||
const model = object(value);
|
||||
return model ? text(model.slug ?? model.id ?? model.model ?? model.name) : null;
|
||||
}
|
||||
|
||||
export function parseOfficialModels(body: unknown): ModelDefinition[] {
|
||||
const root = object(body);
|
||||
const source = root?.models ?? root?.data ?? body;
|
||||
if (!Array.isArray(source)) {
|
||||
throw new Error("Codex model discovery response does not contain a model list");
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
const models: ModelDefinition[] = [];
|
||||
for (const raw of source) {
|
||||
const model = object(raw);
|
||||
if (!model || model.supported_in_api === false || model.supportedInApi === false) continue;
|
||||
if (text(model.visibility)?.toLowerCase() === "hidden") continue;
|
||||
const id = modelId(model);
|
||||
if (!id || seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
const efforts = parseReasoningEfforts(model);
|
||||
const description = text(model.description);
|
||||
const contextWindowTokens = positiveInteger(
|
||||
model.context_window_tokens ?? model.contextWindowTokens ?? model.context_window ??
|
||||
model.contextWindow,
|
||||
);
|
||||
const maxOutputTokens = positiveInteger(
|
||||
model.max_output_tokens ?? model.maxOutputTokens ?? model.max_completion_tokens ??
|
||||
model.maxCompletionTokens,
|
||||
);
|
||||
models.push({
|
||||
id,
|
||||
displayName: text(model.display_name ?? model.displayName ?? model.title ?? model.name) ??
|
||||
id,
|
||||
...(description ? { description } : {}),
|
||||
...(contextWindowTokens !== null ? { contextWindowTokens } : {}),
|
||||
...(maxOutputTokens !== null ? { maxOutputTokens } : {}),
|
||||
capabilities: { thinking: efforts.length > 0, images: true },
|
||||
privateData: { reasoningEfforts: efforts },
|
||||
});
|
||||
}
|
||||
const defaultModel = modelId(
|
||||
root?.default_model ??
|
||||
root?.defaultModel ??
|
||||
root?.default_model_slug ??
|
||||
root?.defaultModelSlug ??
|
||||
root?.primary_model ??
|
||||
root?.primaryModel,
|
||||
);
|
||||
// 把上游默认模型排在最前,让宿主自然选中它。
|
||||
if (defaultModel) {
|
||||
models.sort((left, right) =>
|
||||
Number(right.id === defaultModel) - Number(left.id === defaultModel)
|
||||
);
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
export function reasoningEfforts(model: ModelSnapshot): string[] {
|
||||
const data = object(model.privateData);
|
||||
const efforts = data?.reasoningEfforts;
|
||||
return Array.isArray(efforts) ? efforts.filter((item) => typeof item === "string") : [];
|
||||
}
|
||||
|
||||
export const codexModels: ModelSupport = {
|
||||
list: async ({ resource }, context): Promise<ModelDefinition[]> => {
|
||||
if (!resource) throw new Error("add a ChatGPT account before syncing Codex models");
|
||||
const data = accountData(resource);
|
||||
const response = await context.network.fetch(MODELS_URL, {
|
||||
method: "GET",
|
||||
headers: accountHeaders(data),
|
||||
});
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
throw new Error(`Codex model discovery failed (HTTP ${response.status}): ${response.body}`);
|
||||
}
|
||||
let body: unknown;
|
||||
try {
|
||||
body = JSON.parse(response.body) as JsonValue;
|
||||
} catch {
|
||||
throw new Error("Codex model discovery returned invalid JSON");
|
||||
}
|
||||
const models = parseOfficialModels(body);
|
||||
if (models.length === 0) {
|
||||
throw new Error("Codex model discovery returned no supported models");
|
||||
}
|
||||
return models;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user