ai-chatbot/lib/ai/vip98k-chat-contract.ts

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));
}