74 lines
3.2 KiB
TypeScript
74 lines
3.2 KiB
TypeScript
import { getVip98kInvocationHeaders } from "./vip98k-task-invocation";
|
|
|
|
type RecordValue = Record<string, unknown>;
|
|
export type Vip98kChatInvocation = {
|
|
protocol: "openai-chat" | "openai-responses";
|
|
path: "/v1/chat/completions" | "/v1/responses";
|
|
headers: Record<string, string>;
|
|
};
|
|
|
|
function record(value: unknown): RecordValue | null {
|
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
? (value as RecordValue)
|
|
: null;
|
|
}
|
|
|
|
function array(value: unknown): unknown[] {
|
|
return Array.isArray(value) ? value : [];
|
|
}
|
|
|
|
/** Select an advertised protocol and its own route together, never by model name. */
|
|
export function selectVip98kChatInvocation(value: unknown): Vip98kChatInvocation | null {
|
|
const model = record(value) || {};
|
|
const primary = record(model.invocation);
|
|
const defaultRoute = String(model.default_route_option_id ||
|
|
getVip98kInvocationHeaders(primary?.headers)["X-Relay-Route-Option"] || "");
|
|
const offers = array(model.serviceOffers || model.service_offers).flatMap((value) => {
|
|
const offer = record(value);
|
|
return array(offer?.route_options).map((value) => {
|
|
const route = record(value);
|
|
const invocation = record(route?.invocation);
|
|
return { ...invocation, headers: {
|
|
...getVip98kInvocationHeaders(invocation?.headers),
|
|
...getVip98kInvocationHeaders({ "X-Relay-Route-Option": route?.id }),
|
|
} };
|
|
});
|
|
});
|
|
const candidates = [model.chatInvocation, primary, ...offers, ...array(model.invocations)]
|
|
.map(record)
|
|
.filter((item): item is RecordValue => Boolean(item))
|
|
.filter((item) => {
|
|
const example = record(item.request_example || item.requestExample);
|
|
const modelId = String(model.modelId || model.id || "");
|
|
return (!item.method || String(item.method).toUpperCase() === "POST") &&
|
|
(!item.kind || item.kind === "chat") &&
|
|
(!modelId || !example?.model || example.model === modelId) &&
|
|
(item.path === "/v1/chat/completions" || item.path === "/v1/responses");
|
|
});
|
|
// Prefer the published default route when it supports this protocol. An
|
|
// unscoped protocol example must not discard a route's execution contract.
|
|
candidates.sort((a, b) => {
|
|
const score = (item: RecordValue) => {
|
|
const route = getVip98kInvocationHeaders(item.headers)["X-Relay-Route-Option"];
|
|
return route && route === defaultRoute ? 2 : route ? 1 : 0;
|
|
};
|
|
return score(b) - score(a);
|
|
});
|
|
const selected = candidates[0];
|
|
if (!selected) return null;
|
|
return {
|
|
protocol: selected.path === "/v1/responses" ? "openai-responses" : "openai-chat",
|
|
path: selected.path as Vip98kChatInvocation["path"],
|
|
headers: getVip98kInvocationHeaders(selected.headers),
|
|
};
|
|
}
|
|
|
|
export function vip98kChatSupportsVision(value: unknown): boolean {
|
|
const model = record(value) || {};
|
|
const modes = [...array(model.inputModes || model.input_modes), ...array(model.capabilities)]
|
|
.map((item) => String(item).toLowerCase().replace(/_/g, "-"));
|
|
// A family name alone is not proof that the purchased route accepts images.
|
|
return String(model.modality).toLowerCase() === "multimodal" ||
|
|
modes.some((mode) => ["image", "image-url", "image-input", "image-understanding", "vision", "multimodal"].includes(mode));
|
|
}
|