mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-04 02:52:55 +08:00
130 lines
4.7 KiB
TypeScript
130 lines
4.7 KiB
TypeScript
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;
|
|
},
|
|
};
|